OpenGL ES入门教程(二)之绘制一个平面桌子

news2024/11/26 5:28:23

OpenGL ES入门教程(二)之绘制一个平面桌子

    • 前言
    • 0. OpenGL绘制图形的整体框架概述
    • 1. 定义顶点
    • 2. 定义着色器
    • 3. 加载着色器
    • 4. 编译着色器
    • 5. 将着色器链接为OpenGL程序对象
    • 6. 将着色器需要的数据与拷贝到本地的数组相关联
    • 7. 在屏幕上绘制图形
    • 8. 让桌子有边框的效果

前言

上一篇文章OpenGL ES入门教程(一)编写第一个OpenGL程序,我们创建了自己的第一个OpenGL程序,实现了绘制红色背景的Activity页面,算是OpenGL ES的hello world程序吧。本篇文章基于上一篇文章基础上讲解如何使用OpenGL绘制一张平面桌子,桌子由一个长方形构成,且长方形中间绘制一条线,长方形两头绘制两个点。文章中提到的示例代码我都共享到gitee上了,各位博友可以从我的gitee仓库OpenGL_ES_DEMO下载完整的项目代码,代码都有很详细的提交记录。

0. OpenGL绘制图形的整体框架概述

  1. 要绘制图形,就要有图形的位置坐标数据,OpenGL中称这些位置为顶点,因此,首先需要定义顶点数据
  2. OpenGL如何操作顶点数据呢?答案是通过着色器操作图形数据,因此,第二步我们要定义着色器
  3. 定义完着色器需要加载着色器以供OpenGL使用
  4. OpenGL编译着色器
  5. 将着色器链接为一个OpenGL程序对象
  6. 通过OpenGL程序对象将着色器与图形数据相关
  7. 以上操作算是使用OpenGL的初始化操作,步骤比较固定,编写完成后,就可以开心的绘制图形啦

绘制图形都是在OpenGL渲染器类中完成,所以,本篇文章的所有代码都是在上一篇文章中定义的AirHockeyRenderer类中编写。

1. 定义顶点

OpenGL包括三类基础图形,点,直线,三角形。其余的任何图形,都是由这三种基本图形组成。

因此如果我们想绘制一个前言中所述的平面桌子,可以由两个三角形组成一个长方形,并在长方形的中间绘制一条直线,两端绘制两个点,如下图所示:
在这里插入图片描述
无论是x还是y坐标,OpenGL都会把屏幕映射到[-1,1]的范围内。 即屏幕的左边对应x轴的-1,右边对应+1;屏幕的底边对应y轴的-1,顶边对应+1;因此如果将上面的图形绘制到屏幕中间,需要的顶点坐标如下图所示:
在这里插入图片描述
如上图所示坐标数据,我们将每个顶点(由x坐标和y坐标组成)的数据存储到数组中,上图主要包含的OpenGL基本图形是两个三角形,一条线,两个点,其中三角形的顶点我们统一按照逆时针方向进行存储,这一步数据准备,我可以将它放在渲染器类的构造函数中,最终定义的顶点数据如下:

private Context context;//后面我们加载着色器需要用到该上下文,因此也通过构造函数传进去
public AirHockeyRenderer(Context context)
    {
        this.context = context;

        float[] tableVerticesWithTriangles = {
                /**
                无论是x还是y坐标,OpenGL都会把屏幕映射到[-1,1]的范围内。
                即屏幕的左边对应x轴的-1,右边对应+1;
                屏幕的底边对应y轴的-1,顶边对应+1
                */
                // 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
        };
    }

OpenGL作为本地系统库直接运行在硬件上,无法直接读取java程序中定义的数据,因此我们需要把上面定义的顶点数据复制到本地内存中。具体实现代码如下:

private Context context;
private static final int BYTES_PER_FLOAT = 4;
private final FloatBuffer vertexData;
public AirHockeyRenderer(Context context)
    {
        this.context = context;

        float[] tableVerticesWithTriangles = {
                /**
                无论是x还是y坐标,OpenGL都会把屏幕映射到[-1,1]的范围内。
                即屏幕的左边对应x轴的-1,右边对应+1;
                屏幕的底边对应y轴的-1,顶边对应+1
                */
                // 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
  				//申请本地内存空间大小,单位为字节。tableVerticesWithTriangles中存储的是float类型数据,由32bit组成,即4个字节组成;
                .allocateDirect(tableVerticesWithTriangles.length * BYTES_PER_FLOAT)
                //本地内存空间的一种排序方式
                .order(ByteOrder.nativeOrder())
                //转换为我们需要的FloatBuffer类型
                .asFloatBuffer();
        //将tableVerticesWithTriangles中的数据拷贝到本地内存中
        vertexData.put(tableVerticesWithTriangles);
    }

2. 定义着色器

OpenGL绘制图形的流程称为OpenGL管道(pipeline)。上面我们定义了顶点数据,并将其拷贝到了本地内存,下面就是将本地内存中的图形顶点数据在OpenGL管道进行流动,并通过着色器告诉GPU如何绘制数据。

着色器是一种只能运行在GPU上的特殊类型程序。OpenGL着色器分为顶点着色器和片段着色器两种类型:

  • 顶点着色器(vertex shader)用于处理顶点(主要作用是确定位置);
  • 片段着色器用于处理,由点,线,三角形组成的片段(主要作用是告诉GPU每个片段的最终颜色)

着色器处理完成后(颜色生成),openGL将它们写入一块称为帧缓冲区(frame buffer)的内存块,然后,Android把这分帧缓冲区中的数据显示在屏幕上。

OpenGL管道(pipeline)流程如下:

  • 读取顶点数据 -》执行顶点着色器-》组装图元-》光栅化图元-》执行片段着色器-》写入帧缓冲器-》显示在屏幕上

其中,光栅化技术是指:移动设备的显示屏通过大量像素的堆积(红绿蓝三种颜色不同比例的混合,就足以创造出人眼可见范围内的颜色),在视觉上创造出巨量颜色范围的技术。而OpenGL光栅化就是把每个点,直线,三角形,分解成大量的小片段,通常情况下,一个片段直接映射到屏幕的一个像素。

OpenGL着色器的定义采用着色器特定的语言(语法结构类似C语言),着色器文件后缀名为glsl(OpenGL shader language)。我们在工程的res目录下新建一个raw文件夹,并在raw文件夹中创建顶点着色器文件simple_vertex_shader.glsl和片段着色器文件simple_fragment_shader.glsl,如下图所示:

在这里插入图片描述
顶点着色器文件内容如下:

/*
attribute:定义顶点类型位置数据的特定标识
vec4:一种包含4个分量的向量数据类型(x,y,z,w)
     其中x,y,z代表顶点的三维位置坐标,w是一个特殊坐标,后面会讲解
a_Position:变量名称,该名称后面OpenGL的glGetAttribLocation方法要用到,
           如果修改后面就要一起修改
*/
attribute vec4 a_Position;     		

//和C语言类似,main函数是着色器的入口函数
void main()                    
{   //gl_Position :OpenGL特定的变量名,用于存储我们定义的顶点数据                           
    gl_Position = a_Position;
    //gl_PointSize:OpenGL特定的变量名,用于存储点的大小
    gl_PointSize = 10.0;
}  

片段着色器内容如下:

//OpenGL定义float数据类型的精度(lowp;mediump;highp),就像java代码中浮点型选择float类型还是double类型。
//精度是以性能为代价的,这里选择mediump
precision mediump float; 

/*
uniform:定义片段颜色的一种特殊标识
vec4:一种包含4个分量的向量数据类型(r,g,b,a),分别代码红,绿,蓝,透明度。
     其中rgba的取值范围是0-1,rgba色彩不了解的可以去其它文章了解一下。
u_Color:变量名称,该名称后面OpenGL的glGetUniformLocation方法要用到,
                 如果修改后面就要一起修改
*/      	 								
uniform vec4 u_Color;
          	   								
//和C语言类似,main函数是着色器的入口函数
void main()                    		
{                              	
    gl_FragColor = u_Color;
}

3. 加载着色器

加载着色器其实非常简单就是通过Java IO流的方式将着色器文件中的内容读取为一个字符,以供OpenGL后面编译着色器使用。我以前写过一篇详细的Java IO流文章,如果有兴趣的博友可以移步去看看Java IO流最全详解

为了复用代码,我们定义一个TextResourceReader类,并在类中实现一个静态方法readTextFileFromResource,专用于加载着色器,具体实现代码如下:

public class TextResourceReader {
    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  newLine;
            while ((newLine = bufferedReader.readLine()) != null)
            {
                body.append(newLine);
                body.append("\n");
            }
        }
        catch (IOException e)
        {
            throw new RuntimeException("Could not open resource: " + resourceId, e);
        }
        catch (Resources.NotFoundException e)
        {
            throw new RuntimeException("Resource not found: " + resourceId, e);
        }
        return body.toString();
    }
}

因为加载着色器算是OpenGL绘图的初始化操作,我们可以在surface创建的时候调用一次该逻辑,即在渲染器类重写的onSurfaceCreated方法中调用加载着色器的逻辑,具体代码如下所示:

	@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
        //加载着色器
        String vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader);
        String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader);
    }

4. 编译着色器

编译着色器和将着色器链接到OpenGL程序对象都是比较固定的渲染步骤,为了重复利用它们,我们定义一个ShaderHelper类,专门用于编译着色器,将着色器链接为OpenGL程序对象和验证OpenGL程序对象的有效性。

编译着色的实现流程比较固定,我们只需要会用就行了,不必死记硬背,具体实现代码如下:

     /**
     * Compiles a shader, returning the OpenGL object ID.
     * 1. glCreateShader 创建着色器对象 0代表失败,检查创建状态
     * 2. glShaderSource 向着色器对象中上传着色器源码
     * 3. glCompileShader 着色器对象编译源码
     * 4. glGetShaderiv 获取编译状态,若编译失败,则删除着色器对象id,否则返回着色器对象id
     * 5. glGetShaderInfoLog 获取编译结果的详细信息
     * 6. 如果编译失败,glDeleteShader删除渲染器对象id
     * 7. 编译成功,返回渲染器对象id
     * @param type 着色器类型:顶点着色器 GL_VERTEX_SHADER,片段着色器 GL_FRAGMENT_SHADER
     * @param shaderCode 加载的着色器代码
     * @return
     */
    private static int compileShader(int type, String shaderCode) {
        // Create a new shader object.
        final int shaderObjectId = glCreateShader(type);

        if (shaderObjectId == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Could not create new shader.");
            }

            return 0;
        }

        // Pass in the shader source.
        glShaderSource(shaderObjectId, shaderCode);

        // Compile the shader.
        glCompileShader(shaderObjectId);

        // Get the compilation status.
        final int[] compileStatus = new int[1];
        glGetShaderiv(shaderObjectId, GL_COMPILE_STATUS, compileStatus, 0);

        if (LoggerConfig.ON) {
            // Print the shader info log to the Android log output.
            Log.v(TAG, "Results of compiling source:" + "\n" + shaderCode + "\n:"
                    + glGetShaderInfoLog(shaderObjectId));
        }

        // Verify the compile status.
        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;
        }

        // Return the shader object ID.
        return shaderObjectId;
    }
 
    /**
     * Loads and compiles a vertex shader, returning the OpenGL object ID.
     */
    public static int compileVertexShader(String shaderCode) {
        return compileShader(GL_VERTEX_SHADER, shaderCode);
    }

    /**
     * Loads and compiles a fragment shader, returning the OpenGL object ID.
     */
    public static int compileFragmentShader(String shaderCode) {
        return compileShader(GL_FRAGMENT_SHADER, shaderCode);
    }

因为编译着色器算是OpenGL绘图的初始化操作,我们可以在surface创建的时候调用一次该逻辑,即在渲染器类重写的onSurfaceCreated方法中调用编译着色器的逻辑,具体代码如下所示:

	@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
        //加载着色器
        String vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader);
        String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader);
        //编译着色器
        int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
        int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
    }

5. 将着色器链接为OpenGL程序对象

将着色器链接为OpenGL程序对象的实现流程也比较固定,我们只需要会用就行了,不必死记硬背,具体实现代码如下:

 	/**
     *
     * Links a vertex shader and a fragment shader together into an OpenGL
     * program. Returns the OpenGL program object ID, or 0 if linking failed.
     *
     * 1. glCreateProgram 创建OpenGL链接程序对象,获取对象id, 对象id为0代表创建失败
     * 2. glAttachShader 附上着色器
     * 3. glLinkProgram 链接程序,把着色器联合起来
     * 4. glGetProgramiv 获取链接状态,若成功则返回链接对象id,否则glDeleteProgram删除链接对象
     *
     * @param vertexShaderId 顶点着色器对象id
     * @param fragmentShaderId 片段着色器对象id
     * @return
     */
    public static int linkProgram(int vertexShaderId, int fragmentShaderId) {
        // Create a new program object.
        final int programObjectId = glCreateProgram();

        if (programObjectId == 0) {
            if (LoggerConfig.ON) {
                Log.w(TAG, "Could not create new program");
            }

            return 0;
        }

        // Attach the vertex shader to the program.
        glAttachShader(programObjectId, vertexShaderId);
        // Attach the fragment shader to the program.
        glAttachShader(programObjectId, fragmentShaderId);

        // Link the two shaders together into a program.
        glLinkProgram(programObjectId);

        // Get the link status.
        final int[] linkStatus = new int[1];
        glGetProgramiv(programObjectId, GL_LINK_STATUS, linkStatus, 0);

        if (LoggerConfig.ON) {
            // Print the program info log to the Android log output.
            Log.v(TAG, "Results of linking program:\n"
                    + glGetProgramInfoLog(programObjectId));
        }

        // Verify the link status.
        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;
        }

        // Return the program object ID.
        return programObjectId;
    }

     /**
     * Validates an OpenGL program. Should only be called when developing the application.
     * 1. glValidateProgram验证链接到OpenGL程序对象的有效性
     * 2. glGetProgramiv获取OpenGL程序对象有效性的状态,如果返回0代表无效,否则代表链接OpenGL程序对象成功
     * @param programObjectId
     * @return
     */
    public static boolean validateProgram(int programObjectId) {
        glValidateProgram(programObjectId);

        final int[] validateStatus = new int[1];
        glGetProgramiv(programObjectId, GL_VALIDATE_STATUS, validateStatus, 0);
        Log.v(TAG, "Results of validating program: " + validateStatus[0]
                + "\nLog:" + glGetProgramInfoLog(programObjectId));

        return validateStatus[0] != 0;
    }

因为将着色器链接为OpenGL程序对象算是OpenGL绘图的初始化操作,我们可以在surface创建的时候调用一次该逻辑,即在渲染器类重写的onSurfaceCreated方法中实现链接OpenGL程序对象的逻辑,具体代码如下所示:

    private int program;
	@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
        //加载着色器
        String vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader);
        String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader);
        //编译着色器
        int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
        int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
        //将着色器链接到OpenGL程序
        program = ShaderHelper.linkProgram(vertexShader, fragmentShader);
        //打印OpenGL程序对象的有效性信息
        if (LoggerConfig.ON) {
            ShaderHelper.validateProgram(program);
        }
    }

6. 将着色器需要的数据与拷贝到本地的数组相关联

至此,我们已经完成了所要绘制图形的顶点数据定义,着色器定义、加载、编译,以及OpenGL程序对象获取。下面我们需要通过OpenGL程序对象获取着色器中定义的属性,并将着色器需要的数据与我们拷贝到本地的数据相关联,继而完成OpenGL绘图的所有前置操作。因为这一步仍然属于OpenGL绘图的初始化或者前置工作,我们可以在surface创建的时候调用一次该逻辑,即在渲染器类重写的onSurfaceCreated方法中调用实现该逻辑,具体实现代码如下:

    //这个字符串一定要和片段着色器中定义的属性名一致
    private static final String U_COLOR = "u_Color";
    private int uColorLocation;
    //这个字符串一定要和顶点着色器中定义的属性名一致
    private static final String A_POSITION = "a_Position";
    private int aPositionLocation;
    //每个顶点由两个浮点数组成:x,y
    private static final int POSITION_COMPONENT_COUNT = 2;
	@Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        glClearColor(1.0f, 0.0f, 0.0f, 0.0f);
        
        //加载着色器
        String vertexShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_vertex_shader);
        String fragmentShaderSource = TextResourceReader.readTextFileFromResource(context, R.raw.simple_fragment_shader);
        
        //编译着色器
        int vertexShader = ShaderHelper.compileVertexShader(vertexShaderSource);
        int fragmentShader = ShaderHelper.compileFragmentShader(fragmentShaderSource);
        
        //将着色器链接到OpenGL程序
        program = ShaderHelper.linkProgram(vertexShader, fragmentShader);
        //打印OpenGL程序对象的有效性信息
        if (LoggerConfig.ON) {
            ShaderHelper.validateProgram(program);
        }
        
        /*
        将着色器需要的数据与拷贝到本地的数组相关联
        */
        //告诉OpenGL在绘制任何东西到屏幕上时候,使用这里定义程序
        glUseProgram(program);
        //获取片段着色器中uniform的颜色属性
        uColorLocation = glGetUniformLocation(program, U_COLOR);
        //获取顶点着色器中(attribute)位置属性
        aPositionLocation = glGetAttribLocation(program, A_POSITION);
        //告诉OpenGL从vertexData中读取a_Position的数据
        vertexData.position(0);//将缓冲区数据中的指针指向第一个数据,即从第一个数据开始读
        /*
        将着色器中的位置属性与本地顶点数据相关联。
        aPositionLocation:着色器中定义的位置属性
        POSITION_COMPONENT_COUNT:每次从本地数组中读取两个数据(即x,y代表一个顶点坐标)
        GL_FLOAT:OpenGL采用的数据类型,因为我们定义的是浮点数数组,所以采用GL_FLOAT
        vertexData:要关联的本地数据列表
        */
        glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GL_FLOAT,
                false, 0, vertexData);
        //使能顶点数组
        glEnableVertexAttribArray(aPositionLocation);
    }

glVertexAttribPointer方法所需参数的详细说明如下图所示:
在这里插入图片描述

7. 在屏幕上绘制图形

至此,我们已经完成了使用OpenGL绘制图形的所有必要的前置步骤,下面就是绘制图形了,绘制图形的逻辑在渲染类中重写的onDrawFrame方法中实现。

OpenGL绘制基本图形的步骤如下

  1. 通过glUniform4f方法,更新片段着色器的颜色,该颜色将应用于下面它后面绘制的所有图形,直至再次更新颜色。
    glUniform4f方法的定义如下:
   public static native void glUniform4f(
        int location,//片段着色器的颜色属性
        float x, //rgb色彩的r分量
        float y, //rgb色彩的g分量
        float z, //rgb色彩的b分量
        float w  //rgb色彩的透明度分量
    );
  1. 通过glDrawArrays方法,绘制基本图形
    glDrawArrays方法的定义如下:
   public static native void glDrawArrays(
        int mode, //要绘制的基本图元类型:三角形 GL_TRIANGLES;直线 GL_LINES;点 GL_POINTS
        int first,//从本地顶点数组中读取数据的开始位置
        int count //一共读取多少个顶点,上面glVertexAttribPointer方法中我定义了一个顶点由2个数据组成
    );

因此,如果我们想绘制一个白色的三角形,实现代码如下:

//更新着色器中u_Color的值(白色)
glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
//因为三角形由三个顶点组成,因此我们从本地顶点数据列表中的第一个顶点开始,连续读取3个顶点
glDrawArrays(GL_TRIANGLES, 0, 3);

如果我们想要实现绘制上面提到的如下图形,具体实现代码如下:
在这里插入图片描述

	@Override
    public void onDrawFrame(GL10 gl) {
        glClear(GL_COLOR_BUFFER_BIT);
        /*
        绘制桌子
         */
        //更新着色器中u_Color的值(白色)
        glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
        //参数1:绘制三角形;参数2:从顶点数组的开头开始读顶点;参数3:读取6个顶点(即绘制两个三角形)
        //之前glVertexAttribPointer告诉过OpenGL每个顶点的位置包含两个浮点分量,因此OpenGL会使用vertexData中如下12个浮点数绘制两个三角形
        /**
         // 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);

        /*
        绘制分割线
         */
        //更新u_Color的值(红色)
        glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
        //参数1:绘制直线; 参数2:从顶点数组的第6个顶点之后(即第7个顶点)开始读取;参数3:读取两个顶点
        glDrawArrays(GL_LINES, 6, 2);

        /*
        绘制两个木槌
         */
        //更新u_Color的值(蓝色)
        glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
        glDrawArrays(GL_POINTS, 8, 1);
        //更新u_Color的值(绿色)
        glUniform4f(uColorLocation, 0.0f, 1.0f, 0.0f, 1.0f);
        glDrawArrays(GL_POINTS, 9, 1);
    }

至此,我们已经实现了使用OpenGL绘制一个平面桌子的全部代码,可以从我的gitee仓库OpenGL_ES_DEMO下载完整的项目代码,并执行git reset --hard f1a8e96f0e126814be1a4275459bd7be37c183c0切到文章目前所实现代码的节点,运行程序效果如下:
在这里插入图片描述

8. 让桌子有边框的效果

为了更好的验证我们是否掌握了使用OpenGL灵活绘制图形的能力,可以在以上绘制图形的基础上,再在长方形内添加一个小的长方形,形成桌子边框的效果,各位博友如果能够独立实现这一功能,那么应该对OpenGL基本图形的绘制有了比较熟练的掌握。具体实现思路和代码如下:

  1. 既然要再加一个小长方形,第一步就需要定义它的顶点坐标,添加小长方形后的顶点坐标数组如下:
float[] tableVerticesWithTriangles = {
                /**
                无论是x还是y坐标,OpenGL都会把屏幕映射到[-1,1]的范围内。
                即屏幕的左边对应x轴的-1,右边对应+1;
                屏幕的底边对应y轴的-1,顶边对应+1
                */
                // 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,

                // Triangle 3
                -0.4f, -0.4f,
                0.4f,  0.4f,
                -0.4f,  0.4f,

                // Triangle 4
                -0.4f, -0.4f,
                0.4f, -0.4f,
                0.4f,  0.4f,

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

                // Mallets
                0f, -0.25f,
                0f,  0.25f
        };
  1. 顶点坐标数据有了后,就可以开始使用OpenGL绘制图形了,添加一个小长方形后的绘制代码如下:
@Override
    public void onDrawFrame(GL10 gl) {
        glClear(GL_COLOR_BUFFER_BIT);
        /*
        绘制桌子
         */
        //更新着色器中u_Color的值(蓝色)
        glUniform4f(uColorLocation, 0.0f, 0.0f, 1.0f, 1.0f);
        //参数1:绘制三角形;参数2:从顶点数组的开头开始读顶点;参数3:读取6个顶点(即绘制两个三角形)
        //之前glVertexAttribPointer告诉过OpenGL每个顶点的位置包含两个浮点分量,因此OpenGL会使用vertexData中如下12个浮点数绘制两个三角形
        /**
         // 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);

        /**
         * 绘制第二个内长方形,形成边框的效果
         */
        //更新着色器中u_Color的值(白色)
        glUniform4f(uColorLocation, 1.0f, 1.0f, 1.0f, 1.0f);
        glDrawArrays(GL_TRIANGLES, 6, 6);

        /*
        绘制分割线
         */
        //更新u_Color的值(红色)
        glUniform4f(uColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
        //参数1:绘制直线; 参数2:从顶点数组的第6个顶点之后(即第7个顶点)开始读取;参数3:读取两个顶点
        glDrawArrays(GL_LINES, 12, 2);

        /*
        绘制两个木槌
         */
        //更新u_Color的值(黑色)
        glUniform4f(uColorLocation, 0.0f, 0.0f, 0.0f, 1.0f);
        glDrawArrays(GL_POINTS, 14, 1);
        //更新u_Color的值(绿色)
        glUniform4f(uColorLocation, 0.0f, 1.0f, 0.0f, 1.0f);
        glDrawArrays(GL_POINTS, 15, 1);
    }

下载完整的项目代码后,执行git reset --hard bd8d6607592abcddd1547a5e36a01688772eda64切到实现带边框效果的桌面节点,运行程序效果如下:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1170753.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

立创eda 焊接辅助工具使用

立创EDA为板级EDA设计软件。EDA指的是通过计算机的辅助完成电路原理图、印刷电路板文件等的绘制、制作、仿真设计。 立创EDA是一款基于浏览器的,专为中国人设计的,友好易用的EDA设计工具。起于2010年,完全由中国人独立开发,拥有独…

Ubuntu下安装vscode,并解决终端打不开vscode的问题

Visual Studio Code安装 1,使用 apt 安装 Visual Studio Code 在官方的微软 Apt 源仓库中可用。按照下面的步骤进行即可: 以 sudo 用户身份运行下面的命令,更新软件包索引,并且安装依赖软件: sudo apt update sud…

女孩子穿这种粉粉嫩嫩~的卫衣也太好看了吧

果然女孩子穿这种粉粉嫩嫩的衣服 真的超级有甜美可爱氛围哎 软糯亲肤的面料,上身很舒服哦 时尚polo领加上半拉链设计 既实用又美观,穿脱很方便

如何使用Selenium处理Cookie,今天彻底学会了!

01、cookie介绍 HTTP协议是无状态的协议。一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接,这就意味着服务器无法从连接上跟踪会话。也就是说即使第一次和服务器连接后并且登录成功后,第二次请求…

进程终止(不同情况+如何查看:strerror,echo $?),终止的方法(return,exit,_exit),exit和_exit的不同

目录 进程终止 进程终止是什么 进程终止的情况 代码跑完,结果正确/不正确 提前知道结果 不知道结果 strerror 示例 -- echo $? 代码未跑完,程序崩溃 示例 进程退出方法 return退出码 exit(status) _exit(status) exit()和_exit()的不同 示例 缓冲区位置 进…

华为云RDS数据库(Mysql)不买公网IP无法Navicate连接

前言 最近公司有一个项目甲方为了便宜购买了华为云的ECS服务器与RDS云数据库(Mysql)进行项目部署,实际部署数据库时发现,华为云的数据库需要购买公网IP才能使用Navicate连接数据库(不可思议的我还提交工单确认了一下以…

FlexmonsterPivotTable-2.9.63 LICENSE

FlexmonsterPivotTable-v2.9.63用于网络报告的数据透视表组件,用于可视化业务数据的最强大的 JavaScript 工具 与任何技术堆栈集成 该组件可与任何技术堆栈无缝协作: 与Angular、React、jQuery、Vue等 完美集成 没有服务器端依赖项 只需几行代码 即可开始…

ROS笔记之TF坐标变换

ROS笔记之TF坐标变换 code review! 文章目录 ROS笔记之TF坐标变换一些相关函数的用法tf::TransFormBroadcaster tf1; tf1.sendTransform()tf::StampedTransform()tf::Transform()tf::Vector3()详解br.sendTransform(tf::StampedTransform(tf::Transform(tf::Quaternion::getI…

MacCleanse for Mac:提高Mac性能的必备工具

MacCleanse是一款专为Mac用户设计的强大系统清理垃圾软件,能够全面清理您的系统,提高Mac的速度和性能。它可以帮助您轻松删除各种无用的文件和数据,包括系统缓存、浏览器缓存、下载历史记录、垃圾文件、无效的日志文件和无效的应用程序。通过…

代码随想录算法训练营第23期day40|343. 整数拆分、96.不同的二叉搜索树

目录 一、(leetcode 343)整数拆分 1.动规五部曲 1)确定dp数组(dp table)以及下标的含义 2)确定递推公式 3)dp的初始化 4)确定遍历顺序 5)举例推导dp数组 2.贪心算…

Windows安装WinDbg调试工具

一.下载 微软官网下载SDK的地址,有win11,win10,win8,win7,其他 https://developer.microsoft.com/en-us/windows/downloads/sdk-archive/ 二.安装 打开windbg\Installers\X64 Debuggers And Tools-x64_en-us.msi 要安…

[机缘参悟-117] :万物同源、聚散离合,皆为机缘;生死轮回,皆为自然(天空之眼、上帝视角、佛看众生;系统思维、局外清醒、升维思考;躬身入局、局内低调、降维做事;反思过去,立足当下...)

目录 前言: 一、万物同源、聚散离合,皆为机缘 二、万物的形成与消亡 2.1 粒子的聚合与物质的形成 2.2 粒子的分离与物质的消亡 二、生命的形成 2.1 有机体的聚合与生命的形成 2.2 有机体的分离与生命的消亡 三、人的大脑神经系统与精神世界的形…

uniapp原生插件之安卓圆角组件原生插件

插件介绍 安卓圆角组件原生插件可以解决nvue下圆角不生效和严重锯齿 插件地址 安卓圆角组件原生插件 - DCloud 插件市场 超级福利 uniapp 插件购买超级福利 详细使用文档 uniapp 安卓圆角组件原生插件 用法 在需要使用插件的页面加载以下代码 <leven-radius ref&q…

G761/761系列流量控制伺服阀放大器

两级流量控制机械反馈&#xff08;MFB&#xff09;式伺服阀喷嘴挡板先导级技术&#xff0c;提供高动态性、高分辨率和低迟滞阀芯驱动力高&#xff0c;设计坚固&#xff0c;确保长寿命运行紧凑型设计&#xff0c;只占用装备的有限空间可提供本质安全型&#xff08;G761K和761K&a…

Python基础入门例程28-NP28 密码游戏(列表)

最近的博文&#xff1a; Python基础入门例程27-NP27 朋友们的喜好&#xff08;列表&#xff09;-CSDN博客 Python基础入门例程26-NP26 牛牛的反转列表&#xff08;列表&#xff09;-CSDN博客 Python基础入门例程25-NP25 有序的列表&#xff08;列表&#xff09;-CSDN博客 目录…

NR0521A-NR0521B DC110V大功率继电器 5W以上 JOSEF约瑟

NR系列大功率继电器 系列型号&#xff1a; NR0521B大功率继电器 NR0521A大功率继电器 NR0521型大功率继电器 用途 大功率继电器 NR0521B DC110V 5W以上在电力工程实际应用中&#xff0c;为防止母线电压经过PT二次侧反馈至高压侧&#xff0c;需要在PT二次侧串接PT刀闸重动…

EASYX精确帧率控制

eg1:小球左右摆动的代码 #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <easyx.h> #include <iostream> #include <math.h> #include <stdlib.h> #include <conio.h> #include <time.h> #define PI 3.14、 /*计算时…

正点原子嵌入式linux驱动开发——Linux 块设备驱动

经过之前这些笔记的学习&#xff0c;都是字符设备驱动&#xff0c;本章来学习一下块设备驱动框架&#xff0c;块设备驱动是Linux三大驱动类型之一。块设备驱动要远比字符设备驱动复杂得多&#xff0c;不同类型的存储设备又对应不同的驱动子系统&#xff0c;本章重点学习一下块设…

【C语言初学者周冲刺计划】5.2一个二维数组中的鞍点

目录 1解题思路&#xff1a; 2代码&#xff1a; 3运行代码结果&#xff1a; 4总结&#xff1a; 1解题思路&#xff1a; 解题流程如下&#xff1a; 对每行进行遍历。先找到每行的最大值&#xff0c;然后再确定该最大值是否是所在列的最小值&#xff0c;若满足&#xff0c;则…

Java日期比较大小的3种方式及拓展

目录 一、字符串String的日期比较 二、数值型long比较 三、日期型Date直接比较 四、Date型日期的获取方式 五、Calendar获取年月日【拓展】 一、字符串String的日期比较 String型的日期通过compareTo()来比较&#xff0c;因为String实现了comparable接口 endDate.compare…