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

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,// Mallets0f, -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,// Mallets0f, -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方法中调用加载着色器的逻辑,具体代码如下所示:

	@Overridepublic 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方法中调用编译着色器的逻辑,具体代码如下所示:

	@Overridepublic 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;@Overridepublic 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,yprivate static final int POSITION_COMPONENT_COUNT = 2;@Overridepublic 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_FLOATvertexData:要关联的本地数据列表*/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_POINTSint first,//从本地顶点数组中读取数据的开始位置int count //一共读取多少个顶点,上面glVertexAttribPointer方法中我定义了一个顶点由2个数据组成);

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

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

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

	@Overridepublic 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,// Mallets0f, -0.25f,0f,  0.25f};
  1. 顶点坐标数据有了后,就可以开始使用OpenGL绘制图形了,添加一个小长方形后的绘制代码如下:
@Overridepublic 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.mzph.cn/news/130212.shtml

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

相关文章

【iOS免越狱】利用IOS自动化WebDriverAgent实现自动直播间自动输入

1.目标 由于看直播的时候主播叫我发 666,支持他,我肯定支持他呀,就一直发,可是后来发现太浪费时间了,能不能做一个直播间自动发 666 呢?于是就开始下面的操作。 2.操作环境 iPhone一台 WebDriverAgent …

latex空心小写字母、数字

公式中可用 R \R R、 E \mathbb{E} E 等空心大写字母表示集合、期望等,latex 用 \mathbb 实现。有时想用空心的小写字母(虚数单位,因 i 已用来表示下标)和数字(指示函数用空心 1),此时 \mathbb…

用于 GaN-HEMT 功率器件仿真的 TCAD 方法论

目录 标题:TCAD Methodology for Simulation of GaN-HEMT Power Devices来源:Proceedings of the 26th International Symposium on Power Semiconductor Devices & ICs(14年 ISPSD)GaN-HEMT仿真面临的挑战文章研究了什么文章的创新点文章的研究方法…

asp.net docker-compose添加volume配置

打开docker-compose.override.yml下面添加 volumes:killsb-one-sqldata:external: false 服务下面添加volume配置 volumes:- "./dapr/config/social-client.json:/app/OidcSettings.json" 添加volume配置成功

【LeetCode刷题-队列与栈】--225.用队列实现栈

225.用队列实现栈 class MyStack {Queue<Integer> queue1;Queue<Integer> queue2;public MyStack() {queue1 new LinkedList<Integer>();queue2 new LinkedList<Integer>();}public void push(int x) {queue2.offer(x);while(!queue1.isEmpty()){que…

Docker学习——②

文章目录 1、Docker是什么1.1 Docker本质1.2 Docker的引擎迭代1.3 Docker和虚拟机的区别1.4 Docker 为什么比虚拟机资源利用率高&#xff0c;启动快&#xff1f;1.5 Docker 和 JVM 虚拟化的区别&#xff1f; 2、Docker架构3、Docker生态3.1 新时代软件诉求3.2 Docker 解决方案 …

C#知识总结 基础篇(下)

目录 5类和继承 5.1类继承 5.2访问继承的成员 5.3屏蔽基类的成员 5.4访问基类的成员 5.5虚方法与覆写方法 5.6构造函数的执行顺序 5.7成员访问修饰符 5.8抽象类 5.9密封类与静态类 6.表达式与运算符 6.1运算符和重载 7.结构 7.1结构体的感念。 7.2结构构造函数与…

C++笔记之动态数组的申请和手动实现一个简单的vector

C笔记之动态数组的申请和手动实现一个简单的vector code review! 文章目录 C笔记之动态数组的申请和手动实现一个简单的vector1.C语言中动态数组的申请与使用1.动态数组的申请使用new和delete使用std::vector 1.std::vector的底层实现2.手动实现一个简单的vector:使用一个指向…

Rocky 安装jdk17

1&#xff09;检测jdk是否安装&#xff1a; #运行 java -version如果提示安装&#xff0c;则输入N&#xff0c;跳过 2&#xff09;检测cpu 类型 若未安装查看linux处理器架构&#xff1a; #运行 hostnamectl #或运行 arch 3&#xff09;去官网下载相应的编译版本的Jdk Or…

CCLINK IEFB总线转ETHERNET/IP网络的协议网关使欧姆龙和三菱的数据互通的简单配置方法

想要实现CCLINK IEFB总线和ETHERNET/IP网络的数据互通。 捷米JM-EIP-CCLKIE是一款ETHERNET/IP从站功能的通讯网关&#xff0c;该产品主要功能是实现CCLINK IEFB总线和ETHERNET/IP网络的数据互通。本网关连接到ETHERNET/IP总线和CCLINK IEFB总线上都可以做为从站使用。网关分别…

C++笔记之lambda捕获列表中的‘this‘指针

C笔记之lambda捕获列表中的’this’指针 code review! 捕获this指针的lambda表达式在C中有多种应用场景。以下是一些示例&#xff1a; 异步编程&#xff1a;当您需要在异步操作中访问类的成员变量或成员函数时&#xff0c;可以使用捕获this指针的lambda表达式。例如&#xf…

STM32H750之FreeRTOS学习--------(四)中断管理

四、FreeRTOS中断管理 中断的概念不再过多叙述&#xff0c;学习过逻辑的都知道 中断的执行过程 中断请求 外设产生中断请求&#xff08;GPIO外部中断、定时器中断等&#xff09;响应中断 CPU停止执行当前程序&#xff0c;转而去执行中断处理程序&#xff08;ISR&#xff09;…

linux centos7安装colmap

centos安装colmap 一、安装依赖 sudo yum install \gflags-devel \glog-devel \glew-devel \atlas \atlas-devel \lapack-devel \blas-devel \flann-devel \lz4-devel \sqlite-devel \metis-devel \qt5-qtbase-devel二、编译安装colmap git clone https://github.com/colmap/…

nvm 下载 nodejs 速度慢问题解决

1、找到 nvm 的下载目录&#xff0c;在目录下找到 settings.txt 文件 2、打开 settings.txt 文件 &#xff0c;添加以下代码&#xff1a; node_mirror: https://npm.taobao.org/mirrors/node/ npm_mirror: https://npm.taobao.org/mirrors/npm/添加完成后再去下载即可。

Webpack的入口(entry)和出口(output)

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

Windows 下编译 TensorFlow 2.9.1 CC库

参考 Intel 的 tensorflow 编译指导&#xff0c;不过项目还是可以用 TF原本的&#xff0c;不是一定要选择Intel 的TF版本。 安装 MSVC 2019 安装 Intel OneDNN OneMKL 似乎也可以不安装 ( & ) https://www.intel.cn/content/www/cn/zh/developer/articles/tool/one…

Technology strategy Pattern 学习笔记1-Context: Architecture and Strategy

Context: Architecture and Strategy 1 Architect and Strategist 1.1 three primary concerns of the architect 1.1.1 Contain entropy(熵-混乱程度&#xff0c;不确定性&#xff0c;惊奇程度&#xff0c;不可预测性&#xff0c;信息量等等&#xff09; The architect wh…

Python|Pyppeteer获取威科先行文章链接(21)

前言 本文是该专栏的第21篇,结合优质项目案例持续分享Pyppeteer的干货知识,记得关注。 本文以“威科先行”的信息库为例,笔者将详细介绍使用pyppeteer“自动滑动页面并翻页”获取威科先行的文章链接。如果对pyppeteer的使用以及知识点不太熟悉的同学,可往前查看本专栏前面…

Modelsim 使用教程(5)——Analyzing Waveforms

一、概述 Wave窗口允许我们以HDL波形和数据的形式查看仿真结果。Wave窗口被划分为多个窗格。通过单击并在任意两个窗格之间拖动该条&#xff0c;可以调整路径名窗格、值窗格和波形窗格的大小。 二、加载一个设计&#xff08;Loading a Design&#xff09; 1、打开modelsim 2、…

SQL左连接实战案例

要求&#xff1a;用表df1和表df2的数据&#xff0c;得到df3 一、创建表 CREATE TABLE df1 (姓名 varchar(255) DEFAULT NULL,年龄 int DEFAULT NULL,部门 varchar(255) DEFAULT NULL,id int DEFAULT NULL );CREATE TABLE df2 (部门 varchar(255) DEFAULT NULL,年龄 int DEFAU…