二、OpenGL ES绘制的简单使用

一、几个概念的介绍

  • 着色器语言
    仅适合GPU编程,对于顶点着色器和片元着色器的开发都需要用到着色器语言进行开发

  • 顶点着色器
    顶点着色器(Vertex Shader)是在GPU上运行的小程序。从名称可以看出,可通过处理它们来处理顶点

  • 片段着色器
    片段着色器计算每个像素的颜色和其它属性。它通过应用光照值、凹凸贴图,阴影,镜面高光,半透明等处理来计算像素的颜色并输出。
    它也可改变像素的深度(z-buffering)或在多个渲染目标被激活的状态下输出多种颜色。
    一个片段着色器不能产生复杂的效果,因为它只在一个像素上进行操作,而不知道场景的几何形状。

  • OpenGL坐标系
    OpenGL的坐标系屏幕中间为原点,横x轴(左-1右1),竖y轴(上1下-1),垂直屏幕为z轴,也就是说坐标的有效范围是(-1,1)


本次要绘制的效果图如下:

背景是黑色,白色的是一个长方形,其实是通过2个三角形构成的,然后中间是一条红色的分割线,以及2个小点(红色和蓝色)

要绘制上面的效果,首先需要先定义白色区域的顶点位置

二、在代码中定义顶点

新建Renderer类继承GLSurfaceView.Renderer,首先是定义一个常量用来表示每个顶点有2个分量,然后在init块中定义存储所有顶点位置的数组

package com.example.openglstudy.demo2

import android.content.Context
import android.opengl.GLES20.*
import android.opengl.GLSurfaceView
import com.example.openglstudy.R
import com.example.openglstudy.util.LoggerConfig
import com.example.openglstudy.util.ShaderHelper
import com.example.openglstudy.util.TextResourceReader
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.FloatBuffer
import javax.microedition.khronos.egl.EGLConfig
import javax.microedition.khronos.opengles.GL10

/**
 * @Author: mChenys
 * @Date: 2020/12/11
 * @Description: 自定义的render(渲染器)
 */

private const val POSITION_COMPONENT_COUNT = 2 //用来表示一个顶点有2个分量,也就是(x,y)

class AirHockeyRenderer(val context: Context) : GLSurfaceView.Renderer {


    init {
       
        /**
         * 效果图上的所有顶点的位置
         *
         * (-0.5f,0.5f)------------------(0.5f,0.5f)
         * |             -(0f,0.25f)                |
         * |                                        |
         * (-0.5f,0f)----------------------(0.5f,0f)
         * |                                        |
         * |            -(0f,-0.25f)                |
         * (-0.5f,-0.5f)------------------(0.5f,-0.5f)
         */
        val tableVerticesWithTriangles = floatArrayOf(
            // Triangle 1
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,

            // Triangle 2
            -0.5f, -0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,

            // Line 1
            -0.5f, 0f,
            0.5f, 0f,

            // Mallets
            0f, -0.25f,
            0f, 0.25f
        )

    }


    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
       ...

    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        //设置视口尺寸来告诉OpenGL可以用来渲染的surface的大小
        glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        ..
    }

}

如代码所示,我们采用的是浮点数来表示每个顶点的坐标,这是因为OpenGL中坐标的范围是[-1,1], 由tableVerticesWithTriangles 数组定义可知,先定义了2个三角形,然后是分割线,最后是2个点,其中三角形顶点的定义是按逆时针顺序定义的,这称为卷曲顺序(winding order),因为可以优化性能。

三、如何让数据可以被OpenGL存取?

虽然上面的步骤完成了顶点的定义,但是OpenGL的代码运行环境与java的代码的运行环境是不同的,因此会面临2个问题:
1.java的代码是运行在Dalvik虚拟机上的,运行在虚拟机上的代码是不能直接访问本地环境(native environment),除非通特定的api。
2.Dalvik虚拟机还是用了垃圾回收机制,意味着虚拟机检测到一个变量、对象、或者其他内存片段不再被使用的时候就会在特定时机释放回收掉,然而本地环境(native environment)并不是这样工作的,它不期望内存块会被移来移去或者被自动释放。

解决方案:
1.使用java本地接口(JNI)
2.把内存从java堆复制到本地堆

这里介绍方案2,这种方式是改变内存的分配方式,刚好java有一个特殊的类(ByteBuffer),可以用来分配本地内存块,并且把Java的数据复制到本地内存。
本地内存块可以被本地环境存取,而不受java的垃圾回收器的管控

接着在tableVerticesWithTriangles数组定义的末尾添加如下代码

 vertexData =
            ByteBuffer.allocateDirect(tableVerticesWithTriangles.size * BYTES_PER_FLOAT) //分配的内存大小是以字节为单位的,这个是c/c++同理
                .order(ByteOrder.nativeOrder()) //告诉缓冲区按照本地字节序(native byte order)来组织它的内容,例如大端对齐还是小端对齐
                .asFloatBuffer() //这里很重要,是告诉缓冲区直接操作的是float而不是字节,因此调用该方法会返回一个反映底层字节的FloatBuffer类实例
        vertexData?.put(tableVerticesWithTriangles) //然后调用改方法把Dalvik的内存复制到本地内存中,当进程结束掉的时候,这块内存才会被回收掉

vertexData需要定义在成员位置上

class AirHockeyRenderer(val context: Context) : GLSurfaceView.Renderer {

    private var vertexData: FloatBuffer? = null
    
    init{...}
    ...
}

四、引入OpenGL管道

 上面的步骤我们已经完成了把java数据复制到了OpenGL的本地内存,那么在把图形画到屏幕之前,我们需要用到OpenGL的管道来传递数据,这就需要用到着色器(shader)的子进程,这些着色器会告诉图形处理单元(GPU)如何绘制数据。
 有两种类型的着色器,在绘制任何内容到屏幕之前需要定义它们:
 1.顶点着色器(vertex shader):生成每个顶点的最终位置,针对每个顶点它都会执行一次;一旦最终确定了位置,OpenGL就可以把这些可见顶点的集合组成点、直线以及三角形。
 2.片段着色器(fragment shader):为组成点、直线、三角形的每个片段生成最终的颜色,针对每个片段,它都会执行一次,一个片段就是一个小的,单一颜色的长方形区域,类似于计算机屏幕的像素。
 一旦最后的颜色生成了,OpenGL就会把他们写到一块称为帧缓冲区(frame buffer)的内存块中,然后,Android会把这个帧缓冲区显示到屏幕上。

在res/raw文件夹内重建着顶点色器源代码文件simple_vertex_shader.glsl,内容如下:

attribute vec4 a_Position;
void main(){
    gl_Position = a_Position;
    gl_PointSize = 10.0;
}

GLSL是OpenGL着色器的语法,和C相似
对于我们创建的每一个顶点,着色器都会调用一次,当被调用的时候,它会在a_Position属性里接收当前顶点的位置,这个属性被定义成vec4类型;
一个vec4是包含4个分量,在位置的上下文中,可以认为是4个分量是x,y,z和w坐标,xyz对于一个三维位置,而w是一个特殊的坐标,默认情况下xyz的值是0,w值是1;
gl_Position中存储的值作为当前顶点的最终位置,并把这些顶点组装成点、直线、和三角形;
gl_PointSize = 10.0;表示点的大小,在OpenGL中,点是由4条边组成的,这里是10就是表示4条边的长度。

在res/raw/fragment_shader.glsl创建片段着色器源代码文件simple_fragment_shader.glsl,内容如下:

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

其中,precision mediump float; //这里定义浮点类型,可选lowp(低精度),mediump(中等精度),highp(高精度);
uniform vec4 u_Color;//一个uniform会让每个顶点都使用同一个值,除非我们再次改变它,它也有4个分量,分别是红,绿,蓝和透明度
gl_FragColor = u_Color; //将颜色赋值到输出变量gl_FragColor,OpenGL会使用这个颜色作为当前片段的最终颜色。

ps:OpenGL的颜色值范围是[0,1],1标示最强

OpenGL管道概述

所谓"光栅化"就是把点、直线、三角形分解成大量的小片段,然后映射到移动设备屏幕的像素上,每个像素点其实就是一个独立部件,它由三个独立的子件构成,分别发出红光、绿光、蓝光,由于像素点非常小,人的眼睛会将红、绿、蓝的光混合在一起,从而创建出巨量的色彩范围。

片段着色器的主要目的就是告诉GPU每个片段最终的颜色是什么。

五、加载着色器

前面我们已经将顶点着色器和片段着色器的源代码写到了res/raw资源文件夹内了,现在我们还需要编写工具方法来读取他们,因为最终我们还是需要将他们传递给OpenGL处理的。

object TextResourceReader {
    
    //读取着色器源码字符串
    fun readTextFileFromResource(
        context: Context,
        resourceId: Int
    ): String {
        val body = StringBuilder()
        try {
            var nextLine: String?
            BufferedReader(InputStreamReader(context.resources.openRawResource(resourceId))).use { reader ->
                while (reader.readLine().also { nextLine = it } != null) {
                    body.append(nextLine)
                    body.append("\n")
                }
            }
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return body.toString()
    }
}

工具类编写完成后,回到我们自定义的Renderer类中,在onSurfaceCreated方法中加入读取着色器的处理

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        //调用该方法来设置清屏用的颜色,红色分量设置为1,那么当屏幕被清空时就会显示红色,全0就是黑色
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f)

        //读取着色器的代码
        val vertexShaderSource =
            TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader)
        val fragmentShaderSource =
            TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader)

      ...
}

六、编译着色器

现在我们已经把着色器的源码读取出来了,接下来就是编译每一个着色器了,我们还需新建一个辅助类ShaderHelper ,用来创建新的OpenGL着色器对象,编译着色器源码,并返回代表那段着色器代码的着色器对象。

object ShaderHelper {
    private const val TAG = "ShaderHelper"

    /**
     * 编译顶点着色器代码
     */
    fun compileVertexShader(shaderCode:String):Int{
        return compileShader(GL_VERTEX_SHADER,shaderCode)
    }

    /**
     * 编译片段着色器代码
     */
    fun compileFragmentShader(shaderCode:String):Int{
        return compileShader(GL_FRAGMENT_SHADER,shaderCode)
    }


    /**
     * 真正执行编译着色器的逻辑
     */
    fun compileShader(type: Int, shaderCode: String): Int {
        //创建一个新的着色器对象
        val shaderObjectId = glCreateShader(type)
        if (shaderObjectId == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG,"Could not create new shader.")
            }
            return 0
        }
        //将着色器源代码上传到着色器对象里
        glShaderSource(shaderObjectId, shaderCode)
        //开始编译着色器
        glCompileShader(shaderObjectId)

        //取出编译状态
        val compileStatus = IntArray(1)
        glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS,compileStatus,0)
        if (LoggerConfig.ON) {
            Log.v(TAG,"Result of compiling source:"+shaderCode
                    +" info:"+ glGetShaderInfoLog(shaderObjectId))
        }
        // 验证编译状态
        if (compileStatus[0] == 0) {
            // If it failed, delete the shader object.
            glDeleteShader(shaderObjectId);

            if (LoggerConfig.ON) {
                Log.w(TAG, "Compilation of shader failed.");
            }

            return 0;
        }

        //返回着色器对象的id
        return shaderObjectId;
    }
}

然后回到自定义的Renderer类,调用上面ShaderHelper类编写的编译着色器的方法,继续编辑onSurfaceCreated回调方法:

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        //调用该方法来设置清屏用的颜色,红色分量设置为1,那么当屏幕被清空时就会显示红色,全0就是黑色
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f)

        //读取着色器的代码
        val vertexShaderSource =
            TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader)
        val fragmentShaderSource =
            TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader)

        //编译着色器源代码
        val vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource)
        val fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource)
    ...
}

七、把着色器一起链接进OpenGL程序

片段着色器和顶点着色器是一起工作的,片段用来着色,顶点用来确定位置,没有片段着色器,OpenGL就不知道怎么绘制那些组成每个点、直线、三角的片段;没有顶点着色器,OpenGL就不知道在哪里绘制这些片段。

现在我们还需要将它们链接到OpenGL程序中,继续编辑ShaderHelper类,加入链接逻辑:

/**
     * 将片段和顶点着色器链接到OpenGL程序中
     */
    fun linkProgram(vertexShaderId:Int,fragmentShaderId:Int):Int{
        //先创建程序
        val programObjectId = glCreateProgram()
        if(programObjectId == 0 ){
            if(LoggerConfig.ON)
                Log.w(TAG,"Could not create new program")
            return 0
        }
        //给程序附上着色器
        glAttachShader(programObjectId,vertexShaderId)
        glAttachShader(programObjectId,fragmentShaderId)

        //链接程序
        glLinkProgram(programObjectId)

        //检查是否成功
        val linkStatus = IntArray(1)
        glGetProgramiv(programObjectId, GL_LINK_STATUS,linkStatus,0)
        if (LoggerConfig.ON) {
            Log.v(TAG, "Results of linking program:\n"
                    + glGetProgramInfoLog(programObjectId));
        }

        // 检查链接状态
        if (linkStatus[0] == 0) {
            // If it failed, delete the program object.
            glDeleteProgram(programObjectId);

            if (LoggerConfig.ON) {
                Log.w(TAG, "Linking of program failed.");
            }

            return 0;
        }

        //返回程序对象id
        return programObjectId;
    }

 /**
     * 验证OpenGL程序对象是否有效
     */
    fun validateProgram(programObjectId:Int):Boolean{
        glValidateProgram(programObjectId)
        val validateStatus = IntArray(1)
        glGetProgramiv(programObjectId, GL_VALIDATE_STATUS,validateStatus,0)
        Log.v(TAG,"Result of validating program:"+validateStatus[0]+" Log:"
                + glGetProgramInfoLog(programObjectId))
        return validateStatus[0] !=0
    }

然后回到自定义的Renderer的onSurfaceCreated方法,加入如下代码:

private var program: Int = 0 //OpenGL 程序
... 

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        
        ....

        //把着色器链接到OpenGL的程序中
        program = ShaderHelper.linkProgram(vertexShader, fragmentShader)

        if (LoggerConfig.ON) {
            //校验程序是否有效
            ShaderHelper.validateProgram(program)
        }

        //开始使用OpenGL程序对象
        glUseProgram(program)

        ...
}

八、获取uniform的位置和属性的位置


private const val U_COLOR = "u_Color" //u_Color名称对应uniform vec4 u_Color;中的名字
private const val A_POSITION = "a_Position" //u_Color名称对应attribute vec4 a_Position;中的名字

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
    ...
    //获取uniform的位置
    uColorLocation = glGetUniformLocation(program, U_COLOR)

    //获取属性位置
    aPositionLocation = glGetAttribLocation(program, A_POSITION)
    
    ...
    
}

九、关联属性与顶点数据的数组

override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
       ...

        //关联属性与顶点数据的数组
        vertexData?.let {
            it.position(0) //设置缓冲区当前指针位置为开始位置,设置0也就是从缓冲区的开头处开始读
            //告诉OpenGL可以在缓冲区vertexData中获取a_Position对应的数据
            glVertexAttribPointer(
                aPositionLocation,//指定属性的位置
                POSITION_COMPONENT_COUNT,//每个顶点的位置包含几个浮点数的分量,这里是2个,因为一个坐标有x和y
                GL_FLOAT, //设置数据类型
                false,  //当使用正数类型才需要设置true
                0, //只有当一个数组存有多个属性的时才有意义,当前只有一个属性,传0即可
                it //这个参数就是缓冲区本身了,指定OpenGL用于读取的缓冲区
            )
        }

        //使能顶点数组,这样OpenGL就知道去哪里找需要的数据了
        glEnableVertexAttribArray(aPositionLocation)

    }

十、开始绘制

继续编辑自定义Renderer类,完善onDrawFrame方法

override fun onDrawFrame(gl: GL10?) {
        //调用下面方法清空屏幕,这会清除屏幕上的所有颜色,并调用之前glClearColor定义的颜色来填充整个屏幕
        glClear(GL_COLOR_BUFFER_BIT)
        //绘制桌子
        //更新着色器代码中u_Color的值,和属性不同,uniform的分量没有默认值,我们要花一张白色的桌子作为开始,
        //那就要将红,绿,蓝的值设置最大,也就是1,透明度可以不管
        glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f)
        /**
         * 指定了颜色后,就可以开始绘制了,
         * 参数1是告诉OpenGL要绘制的内容,这里是三角形
         * 参数2是告诉OpenGL从顶点数组的开头处开始读取顶点
         * 参数3是告诉OpenGL读入6个顶点,因为一个三角形有3个顶点,我们绘制矩形需要2个三角形,所以需要6个点
         * 也就是:
         *    // Triangle 1
         *     -0.5f, -0.5f,
         *     0.5f, 0.5f,
         *    -0.5f, 0.5f,
         *    // Triangle 2
         *    -0.5f, -0.5f,
         *    0.5f, -0.5f,
         *    0.5f, 0.5f,
         */
        glDrawArrays(GL_TRIANGLES, 0, 6)

        //绘制桌子中间的分割线,红色
        glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
        /**
         * 绘制直线,从数组的第6组开始,共2个顶点,也就是
         * // Line 1
         *   -0.5f, 0f,
         *   0.5f, 0f,
         */
        glDrawArrays(GL_LINES, 6, 2)

        //绘制一个点,绘制成蓝色
        glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f)
        //绘制点
        glDrawArrays(GL_POINTS, 8, 1)

        //绘制另一个点,绘制成红色
        glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f)
        glDrawArrays(GL_POINTS, 9, 1)

    }

完整的自定义Renderer类如下:

private const val POSITION_COMPONENT_COUNT = 2 //定义一个常量
private const val BYTES_PER_FLOAT = 4 //float类型占4个字节大小
private const val U_COLOR = "u_Color" //u_Color名称对应uniform vec4 u_Color;中的名字
private const val A_POSITION = "a_Position" //u_Color名称对应attribute vec4 a_Position;中的名字

class AirHockeyRenderer(val context: Context) : GLSurfaceView.Renderer {

    private var program: Int = 0 //OpenGL 程序
    private var vertexData: FloatBuffer? = null
    private var uColorLocation: Int = 0; //uniform的位置
    private var aPositionLocation: Int = 0;

    init {
       /**
         * (-0.5f,0.5f)------------------(0.5f,0.5f)
         * |             -(0f,0.25f)               |
         * |                                       |
         * (-0.5f,0f)----------------------(0.5f,0f)
         * |                                       |
         * |            -(0f,-0.25f)               |
         * (-0.5f,-0.5f)------------------(0.5f,-0.5f)
         */
        val tableVerticesWithTriangles = floatArrayOf(
            // Triangle 1
            -0.5f, -0.5f,
            0.5f, 0.5f,
            -0.5f, 0.5f,

            // Triangle 2
            -0.5f, -0.5f,
            0.5f, -0.5f,
            0.5f, 0.5f,

            // Line 1
            -0.5f, 0f,
            0.5f, 0f,

            // Mallets
            0f, -0.25f,
            0f, 0.25f
        )


        vertexData =
            ByteBuffer.allocateDirect(tableVerticesWithTriangles.size * BYTES_PER_FLOAT) //分配的内存大小是以字节为单位的,这个是c/c++同理
                .order(ByteOrder.nativeOrder()) //告诉缓冲区按照本地字节序(native byte order)来组织它的内容,例如大端对齐还是小端对齐
                .asFloatBuffer() //这里很重要,是告诉缓冲区直接操作的是float而不是字节,因此调用该方法会返回一个反映底层字节的FloatBuffer类实例
        vertexData?.put(tableVerticesWithTriangles) //然后调用改方法把Dalvik的内存复制到本地内存中,当进程结束掉的时候,这块内存才会被回收掉

    }


    override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
        //调用该方法来设置清屏用的颜色,红色分量设置为1,那么当屏幕被清空时就会显示红色,全0就是黑色
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f)

        //读取着色器的代码
        val vertexShaderSource =
            TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader)
        val fragmentShaderSource =
            TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader)

        //编译着色器源代码
        val vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource)
        val fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource)

        /**
         * 把着色器链接到OpenGL的程序中
         * 片段着色器和顶点着色器是一起工作的,片段用来着色,顶点用来确定位置
         */
        program = ShaderHelper.linkProgram(vertexShader, fragmentShader)

        if (LoggerConfig.ON) {
            //校验程序是否有效
            ShaderHelper.validateProgram(program)
        }

        //开始使用OpenGL程序对象
        glUseProgram(program)

        //获取uniform的位置
        uColorLocation = glGetUniformLocation(program, U_COLOR)

        //获取属性位置
        aPositionLocation = glGetAttribLocation(program, A_POSITION)

        //关联属性与顶点数据的数组
        vertexData?.let {
            it.position(0) //设置缓冲区当前指针位置为开始位置,设置0也就是从缓冲区的头部开始读
            //告诉OpenGL可以在缓冲区vertexData中获取a_Position对应的数据
            glVertexAttribPointer(
                aPositionLocation,//指定属性的位置
                POSITION_COMPONENT_COUNT,//每个顶点的位置包含几个浮点数的分量,这里是2个,因为一个坐标有x和y
                GL_FLOAT, //设置数据类型
                false,  //当使用正数类型才需要设置true
                0, //只有当一个数组存有多个属性的时才有意义,当前只有一个属性,传0即可
                it //这个参数就是缓冲区本身了,指定OpenGL用于读取的缓冲区
            )
        }

        //使能顶点数组,这样OpenGL就知道去哪里找需要的数据了
        glEnableVertexAttribArray(aPositionLocation)

    }

    override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
        //设置视口尺寸来告诉OpenGL可以用来渲染的surface的大小
        glViewport(0, 0, width, height)
    }

    override fun onDrawFrame(gl: GL10?) {
        //调用下面方法清空屏幕,这会清除屏幕上的所有颜色,并调用之前glClearColor定义的颜色来填充整个屏幕
        glClear(GL_COLOR_BUFFER_BIT)
        //绘制桌子
        //更新着色器代码中u_Color的值,和属性不同,uniform的分量没有默认值,我们要花一张白色的桌子作为开始,
        //那就要将红,绿,蓝的值设置最大,也就是1,透明度可以不管
        glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f)
        /**
         * 指定了颜色后,就可以开始绘制了,
         * 参数1是告诉OpenGL要绘制的内容,这里是三角形
         * 参数2是告诉OpenGL从顶点数组的开头处开始读取顶点
         * 参数3是告诉OpenGL读入6个顶点,因为一个三角形有3个顶点,我们绘制矩形需要2个三角形,所以需要6个点
         * 也就是:
         *    // Triangle 1
         *     -0.5f, -0.5f,
         *     0.5f, 0.5f,
         *    -0.5f, 0.5f,
         *    // Triangle 2
         *    -0.5f, -0.5f,
         *    0.5f, -0.5f,
         *    0.5f, 0.5f,
         */
        glDrawArrays(GL_TRIANGLES, 0, 6)

        //绘制桌子中间的分割线,红色
        glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
        /**
         * 绘制直线,从数组的第6组开始,共2个顶点,也就是
         * // Line 1
         *   -0.5f, 0f,
         *   0.5f, 0f,
         */
        glDrawArrays(GL_LINES, 6, 2)

        //绘制一个点,绘制成蓝色
        glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f)
        //绘制点
        glDrawArrays(GL_POINTS, 8, 1)

        //绘制另一个点,绘制成红色
        glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f)
        glDrawArrays(GL_POINTS, 9, 1)

    }


}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值