Modern OpenGL - 顶点数组、属性与绑定点(OpenGL 4.5+)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Modern OpenGL - 顶点数组、属性与绑定点(OpenGL 4.5+)相关的知识,希望对你有一定的参考价值。

参考技术A

上一节 我们讲了如何在Modern OpenGL下渲染矩形体,但其中用到的主要是OpenGL 3.x中内容。OpenGL 4.x增添了很多新内容,并且一部分3.x的内容得到改进。本节中会详细讲解新版和与旧版的区别与联系,并给出应用最新技术的例子。

OpenGL 4.5中给了我们DSA,可以在调用方法时直接传入要操作的OpenGL对象,而不再需要绑定操作,更加高效。与之对应的,所有 glGen* 方法均不再使用,因为这些方法只生成了一个对象(实质上是分配了一个ID),并没有定义这个对象的类型。在4.5以前,一个对象的定义发生在首次绑定时。例如:

从OpenGL 4.5起,由于不再需要绑定操作,所有 创建 OpenGL对象的操作都被 glCreate* 所替代,例如:

如果依然使用 glGen* 而不进行首次绑定,则该OpenGL对象是无效对象,操作时会报出GL_INVALID_OPERATION错误。

Modern OpenGL渲染中必须使用VAO,它无处不在。VAO本身不存储任何顶点数据,它会保存我们要渲染时所需要的顶点的 定义、规格与配置 。一旦我们配置好了一个VAO,只要绑定它,就可以直接调用渲染函数,而不需要调用任何定义/配置类的函数。

顶点属性是顶点着色器(Vertex Shader)的输入。在Modern OpenGL中,我们必须自己定义我们需要哪些顶点属性。每定义的一个属性,都可以叫做通用顶点属性。与之对应的,在传统固定管线中,顶点着色器存在内置顶点属性,但它们都已经废弃。所以我们说的顶点属性都是指通用顶点属性,它们的名字、类型都是完全自定义的。例如,我们的顶点着色器:

顶点属性用 in 表示, location = 0 显式指定了属性索引(attribute index)是0,location不一定连续。属性名在这种情况下仅在shader内使用。

顶点缓冲绑定点是在一个VAO内共享的,我们可以将一种配置绑定到一个绑定点上,再将一个或多个顶点属性与之绑定,这样就可以随时切换顶点属性而不需要重新配置缓冲。也就是说,我们定义一个绑定点,和它如何从VBO中读取数据,这个定义信息会被存入VAO,关系图如下:

比如:我们定义0号绑定点对应12个字节大小,然后我们的VBO创建为vbo_first,它的数据是每12个字节一组顶点属性,那么offset是0,stride为12。如果它第8到20字节,28到40字节以此类推,是我们需要的数据的话,那么offset是8(第一组数据是8开始),stride是20(8到28是20, 20到40是20)。剩下的一个个8字节(0到8,20到28...)也许是其他地方需要的数据。因为一个VBO可以存任何东西,VBO用一个还是多个,数据怎么存放,可自由设置。

重点来了,虽然我们在shader中定义了顶点属性,但OpenGL不知道我们是如何定义的,也就不知道怎么把数据传给shader的attribute,所以我们必须指定它们。

第一个参数传入我们的VAO。attribindex传入属性索引,也就是顶点着色器中(location = 几)的几。

注意:上面的函数最终会把数据转成浮点型。如果使用 glVertexArrayAttribIFormat (多了一个I, Integer),则会转成整数类型,并且 type 只能 GL_BYTE , GL_UNSIGNED_BYTE , GL_SHORT , GL_UNSIGNED_SHORT , GL_INT GL_UNSIGNED_INT 。这个方法不含 normalized 参数。

此外还有一个relativeoffset参数,相对偏移量,也就是在绑定点中的偏移量,而不是VBO中的偏移量。我们刚刚定义了绑定点0占据12个字节,这样的话我们可以把之前的 vec2 a_Pos vec4 a_Color 都绑定到这个绑定点上,如果我们的VBO是前8个字节是坐标,后4个字节是颜色,那么就分别调用:

在VBO中存储后4个字节为RGBA颜色值,标准化到浮点型,因为unsigned int是0~255,所以转换时会除255得到shader中使用的颜色值。8是因为坐标2两个浮点值8字节,一个绑定点是12字节。

别忘了, glVertexArrayAttribFormat 只定义了属性格式,要绑定到绑定点上,还需要调用 glVertexArrayAttribBinding

第一个参数是VAO,第二个是属性索引,第三个是绑定点的索引。这里我们就需要把0和1号属性全都绑定到0号绑定点:

假设我们顶点着色器中还有2号和3号属性,也是vec2和vec4。那么只需最开始调用glVertexArrayAttribFormat,然后在需要切换的时候调用glVertexArrayAttribBinding切换绑定即可。你会发现,我们并没有重新配置绑定点,也就是怎么从VBO中读取。而且,我们从来没绑定过VBO,它只是作为参数传入。

如果需要索引绘制(indexed drawing),我们还要将所使用的EBO配置进VAO:

这样一来,所有的配置都存进了VAO,由多个VBO存放顶点数据,只要绑定VAO即可渲染,渲染循环如下:

要更新顶点数据,只需在下一帧渲染开始前使用 glNamedBufferSubData 更新VBO即可。

我的OpenGL学习进阶之旅顶点属性顶点数组

顶点数据也称作顶点属性,指定每个顶点的数据。这种逐顶点数据可以为每个顶点指定,也可以用于所有顶点的常量。
例如,如果你想要绘制固定颜色的三角形(如下图,假定颜色为黑色),可以指定一个常量值,用于三角形的全部3个顶点。但是,组成三角形的3个顶点的位置不同,所以我们必须指定一个顶点数组来存储3个位置值。

一、 指定顶点属性数据

顶点属性数据可以用一个顶点数组对每个顶点指定,也可以将一个常量值用于一个图元的所有顶点。

所有OpenGL ES 3.0实现必须支持最少16个顶点属性。应用程序可以查询特定实现支持的顶点属性的准确数量。

下面代码说明应用程序如何查询OpenGL ES 3.0 实现 真正支持的顶点属性数量、

	GLint maxVertexAttribs;// n will be >= 16
	glGetIntegerv( GL_MAX_VERTEX_ATTRIBS, &maxVertexAttribs);

1.1 常量顶点属性

常量顶点属性对于一个图元的所有顶点都相同,所以对一个图元的所有顶点只需指定一个值。
可以用如下任何一个函数指定:

	void glVertexAttrib1f (GLuint index, GLfloat x);
	void glVertexAttrib1fv (GLuint index, const GLfloat *v);
	
	void glVertexAttrib2f (GLuint index, GLfloat x, GLfloat y);
	void glVertexAttrib2fv (GLuint index, const GLfloat *v);
	
	void glVertexAttrib3f (GLuint index, GLfloat x, GLfloat y, GLfloat z);
	void glVertexAttrib3fv (GLuint index, const GLfloat *v);
	
	void glVertexAttrib4f (GLuint index, GLfloat x, GLfloat y, GLfloat z, GLfloat w);
	void glVertexAttrib4fv (GLuint index, const GLfloat *v);


glVertexAttrib* 命令用于加载index指定的通用属性。

  • glVertexAttrib1fglVertexAttrib1fv 函数在通用顶点属性中加载 (x, 0.0, 0.0, 1.0)
  • glVertexAttrib2fglVertexAttrib2fv 函数在通用顶点属性中加载 (x, y, 0.0, 1.0)
  • glVertexAttrib3fglVertexAttrib3fv 函数在通用顶点属性中加载 (x, y, z, 1.0)
  • glVertexAttrib4fglVertexAttrib4fv 函数在通用顶点属性中加载 (x, y, z, w)

在实践中,常量顶点属性提供与使用标量/向量统一变量等价的功能,两者都是可以接受的选择。

1.2 顶点数组

顶点数组指定每个顶点的属性,是保存在应用程序地址空间(OpenGL ES 称为客户空间) 的缓冲区。
它们作为顶点缓冲区对象的基础,提供指定顶点属性数据的一种高效、灵活的手段。

顶点数组用glVertexAttribPointerglVertexAttribIPointer函数指定。

void glVertexAttribPointer (GLuint index, GLint size,
								 GLenum type, 
								 GLboolean normalized, 
								 GLsizei stride, 
								 const void *pointer);


void glVertexAttribIPointer (GLuint index, GLint size, 
								GLenum type, 
								GLsizei stride, 
								const void *pointer);


参数说明:

  • index
    指定通用顶点属性索引。这个值的范围从0到支持的最大顶点属性数-1。

  • size
    顶点数组中为索引引用的顶点属性所指定的分量数量。有效值为1~4

  • type
    数据格式。两个函数都包括的有效值是:

    #define GL_BYTE                           0x1400
    #define GL_UNSIGNED_BYTE                  0x1401
    #define GL_SHORT                          0x1402
    #define GL_UNSIGNED_SHORT                 0x1403
    #define GL_INT                            0x1404
    #define GL_UNSIGNED_INT                   0x1405
    

    glVertexAttribPointer的有效值还包括:

    #define GL_HALF_FLOAT                     0x140B
    #define GL_FLOAT                          0x1406
    #define GL_FIXED                          0x140C
    #define GL_INT_2_10_10_10_REV             0x8D9F
    #define GL_UNSIGNED_INT_2_10_10_10_REV    0x8368
    
  • normalized
    仅限glVertexAttribPointer函数,用于标识非浮点数据格式在转换为浮点值时是否应该规范化。对于glVertexAttribIPointer ,这些值被当做整数对待

  • stride
    每个顶点由size指定的顶点属性分量顺序存储。stride指定顶点索引I和(I+1)表示的顶点数据之间的位移。如果stride为0,则每个顶点的属性数据顺序存储。如果stride大于0,则使用该值作为获取下一个索引表示的顶点数据的跨距。

  • pointer
    如果使用客户端顶点数组,则是保存顶点属性数据的缓冲区指针。如果使用顶点缓冲区对象,则表示该缓冲区内的偏移量。

接下来,我们介绍几个示例,说明如何用glVertexAttribPointer函数指定顶点属性。

1.3 分配和存储顶点属性数据有两种常用的方法

分配和存储顶点属性数据有两种常用的方法:

  • 结构数组
    结构数组:在一个缓冲区中存储顶点属性。结构表示顶点的所有属性,每个顶点由一个属性的数组。
  • 数组结构
    数组结构:在单独的缓冲区中保存每个顶点属性。

假定每个顶点有4个顶点属性:位置、法线和两个纹理坐标。 这些属性一起保存在为所有顶点分配的一个缓冲区中。

  • 顶点位置属性: 以3个浮点数的向量(x,y,z) 的形式指定。
  • 顶点法线: 也以3个浮点数的向量(x,y,z) 的形式指定。
  • 每个纹理坐标: 以2个浮点数组成的向量(s,t)的形式指定。

下图给出了这个缓冲区的内存布局。


在这个例子中,缓冲区的跨距为组成顶点的所有属性的总大小。(一个顶点等于10个浮点数或者40个字节:12个字节用于位置、12个字节用于法线,8个字节用于Tex0,8个字节用于Tex1)

下面的例1描述了如何用glVertexAttribPointer指定这4个顶点属性。

注意:这里介绍如何使用客户端顶点数组,以便解释逐顶点数据指定的概念。
但是我们建议应用程序使用顶点缓冲区对象 ,避免使用客户端顶点数组,以实现最佳性能。

  • 在OpenGL ES 3.0 中支持 客户端顶点数组只是为了与 OpenGL ES 2.0兼容。
  • 在OpenGL ES 3.0 中,总是建议使用顶点缓冲区对象。

1.3.1 例1: 结构数组

//
// Created by OuyangPeng on 2021/11/17 0017.
//
#define VERTEX_POS_SIZE             3     // x, y and z
#define VERTEX_NORMAL_SIZE          3     // x, y and z
#define VERTEX_TEXCOORD0_SIZE       2     // s and t
#define VERTEX_TEXCOORD1_SIZE       2     // s and t

#define VERTEX_POS_INDX             0
#define VERTEX_NORMAL_INDX          1
#define VERTEX_TEXCOORD0_INDX       2
#define VERTEX_TEXCOORD1_INDX       3

// the following 4 defines are used to determine the locations
// of various attributes if vertex data are stored as an array
// of structures
#define VERTEX_POS_OFFSET             0
#define VERTEX_NORMAL_OFFSET          3
#define VERTEX_TEXCOORD0_OFFSET       6
#define VERTEX_TEXCOORD1_OFFSET       8

#define VERTEX_ATTRIB_SIZE  (VERTEX_POS_SIZE + \\
                                VERTEX_NORMAL_SIZE + \\
                                VERTEX_TEXCOORD0_SIZE + \\
                                VERTEX_TEXCOORD1_SIZE)

float *p = (float*) malloc(numVertices * VERTEX_ATTRIB_SIZE * sizeof(float));

// position is vertex attribute 0
glVertexAttribPointer(VERTEX_POS_INDX,VERTEX_POS_SIZE,
                      GL_FLOAT,GL_FALSE,
                      VERTEX_ATTRIB_SIZE * sizeof(float),
                      p);

// normal is vertex attribute 1
glVertexAttribPointer(VERTEX_NORMAL_INDX,VERTEX_NORMAL_SIZE,
                        GL_FLOAT,GL_FALSE,
                        VERTEX_ATTRIB_SIZE * sizeof(float),
                        p+VERTEX_NORMAL_OFFSET);

// texture coordinate 0  is vertex attribute 2
glVertexAttribPointer(VERTEX_TEXCOORD0_INDX,VERTEX_TEXCOORD0_SIZE,
                    GL_FLOAT,GL_FALSE,
                    VERTEX_ATTRIB_SIZE * sizeof(float),
                    p+VERTEX_TEXCOORD0_OFFSET);

// texture coordinate 1 is vertex attribute 3
glVertexAttribPointer(VERTEX_TEXCOORD1_INDX,VERTEX_TEXCOORD1_SIZE,
                    GL_FLOAT,GL_FALSE,
                    VERTEX_ATTRIB_SIZE * sizeof(float),
                    p+VERTEX_TEXCOORD1_OFFSET);


1.3.2 例2: 数组结构

//
// Created by OuyangPeng on 2021/11/17 0017.
//
#define VERTEX_POS_SIZE             3     // x, y and z
#define VERTEX_NORMAL_SIZE          3     // x, y and z
#define VERTEX_TEXCOORD0_SIZE       2     // s and t
#define VERTEX_TEXCOORD1_SIZE       2     // s and t

#define VERTEX_POS_INDX             0
#define VERTEX_NORMAL_INDX          1
#define VERTEX_TEXCOORD0_INDX       2
#define VERTEX_TEXCOORD1_INDX       3

float *position = (float*) malloc(numVertices * VERTEX_POS_SIZE * sizeof(float));
float *normal = (float*) malloc(numVertices * VERTEX_NORMAL_SIZE * sizeof(float));
float *texcoord0 = (float*) malloc(numVertices * VERTEX_TEXCOORD0_SIZE * sizeof(float));
float *texcoord1 = (float*) malloc(numVertices * VERTEX_TEXCOORD1_SIZE * sizeof(float));

// position is vertex attribute 0
glVertexAttribPointer(VERTEX_POS_INDX,VERTEX_POS_SIZE,
                      GL_FLOAT,GL_FALSE,
                      VERTEX_POS_SIZE * sizeof(float),
                      position);

// normal is vertex attribute 1
glVertexAttribPointer(VERTEX_NORMAL_INDX,VERTEX_NORMAL_SIZE,
                        GL_FLOAT,GL_FALSE,
                        VERTEX_NORMAL_SIZE * sizeof(float),
                        normal);

// texture coordinate 0  is vertex attribute 2
glVertexAttribPointer(VERTEX_TEXCOORD0_INDX,VERTEX_TEXCOORD0_SIZE,
                    GL_FLOAT,GL_FALSE,
                    VERTEX_TEXCOORD0_SIZE * sizeof(float),
                    texcoord0);

// texture coordinate 1 is vertex attribute 3
glVertexAttribPointer(VERTEX_TEXCOORD1_INDX,VERTEX_TEXCOORD1_SIZE,
                    GL_FLOAT,GL_FALSE,
                    VERTEX_TEXCOORD1_SIZE * sizeof(float),
                    texcoord1);


1.4 如何存储顶点的不同属性

1.4.1 结构数组和数组结构,哪种分配方法更高效?

我们已经描述了两种常用的顶点属性存储方法: 结构数组和数组结构。
问题是,对于OpenGL ES 3.0硬件实现,哪种分配方法更高效? 在大部分情况下,答案是结构数组。
原因是,每个顶点的属性数据可以顺序方式读取,这最有可能造成高效的内存访问模式。

1.4.2 使用结构数组的缺点

使用结构数组的缺点在应用程序需要修改特定属性时变得很明显。 如果顶点属性数据的一个子集需要修改(例如,纹理坐标),这将造成顶点缓冲区的跨距更新。当顶点缓冲区以缓冲区对象的形式提供时,需要重新加载整个顶点属性缓冲区。

  • 如何解决?
    可以通过将动态的顶点属性保存在单独的缓冲区来避免这种效率低下的情况。

1.5 顶点属性使用哪种数据格式

glVertexAttribPointer 中用type参数指定的顶点属性数据格式不仅影响顶点属性数据的图形内存存储要求,而且影响整体性能,这是渲染帧所需内存带宽的一个函数。

数据空间占用越小,需要的内存带宽越小。

OpenGL ES 3.0 支持名为 GL_HALF_FLOAT的16位浮点顶点格式。建议在应用程序中尽可能使用 GL_HALF_FLOAT

  • 纹理坐标、法线、副法线、切向量等都是使用 GL_HALF_FLOAT存储每个分量的候选。
  • 颜色可以存储为 GL_UNSIGNED_BYTE,每个顶点颜色具有4个分量。
  • 建议使用GL_HALF_FLOAT存储顶点位置,但是发现这种选择在不少情况下都不可行。对于这些情况,顶点位置可以存储为GL_FLOAT

1.6 glVertexAttribPointer 中的规范化标志如何工作

在用于顶点着色器之前,顶点属性在内部保存为单精度浮点数。如果数据类型表示顶点属性不少浮点数,顶点属性将在用于顶点着色器之前转换为单精度浮点数。

规范化标志控制非浮点顶点属性数据到单精度浮点值的转换。

  • 如果规范化标志位假,则顶点数据被直接转换为浮点值。
    这类似于将非浮点类型的变量转换为浮点变量。下面代码提供了一个例子:
GLfloat f;
GLbyte b;
f = (GLfloat) b; // f represents values in the range [ -128.0 , 127.0 ]
  • 如果规范化标志位真,且如果数据类型为GL_BYTE、GL_SHORT或者GL_FIXED,则顶点数据被映射到[ -1.0, 1.0 ]范围内。如果数据类型为GL_UNSIGNED_BYTE 或者 GL_UNSIGNED_SHORT,则被映射到[ 0.0, 1.0 ]范围内。

下表说明了设置规范化标志时非浮点数据类型的转换,表中第2列的c值指的是第1列中的指定格式的一个值。


在顶点着色器中,也有可能按照整数的形式访问整数型顶点属性数据,而不将其转换为浮点数。
在这种情况下,将使用glVertexAttribIPointer 函数,顶点属性应该在顶点着色器中声明为一种整数类型。

1.7 在常量顶点属性和顶点数组之间选择

应用程序可以让OpenGL ES使用常量顶点属性或者来自顶点数组的数据。
下图描述了这在 OpenGL ES 3.0 中的实现方式。


glEnableVertexAttribArrayglDisableVertexAttribArray命令分别用于启用禁用通用顶点属性数组。
如果某个通用属性索引的顶点属性数组被禁用,则将使用为该索引指定的常量顶点属性数组。

void glDisableVertexAttribArray (GLuint index);

void glEnableVertexAttribArray (GLuint index);
  • index
    指定通用顶点属性索引。这个值的范围从0到支持的最大顶点属性数量减1。

1.7.1 例3 使用常量和顶点数组属性

例3 说明如何绘制一个三角形,该三角形的一个顶点属性是常量,其他属性用顶点数组指定。

下面的代码可以在 https://github.com/ouyangpeng/OpenGLESDemo/ 中去查看源代码

  • NativeTriangle2.h
#pragma once
#include "../../utils/GLUtils.h"
namespace NAMESPACE_NativeTriangle2 
	class NativeTriangle 

	public:
		NativeTriangle();

		~NativeTriangle();

		void create();

		void change(int width, int height);

		void draw();

	private:
		GLuint mProgram;
		int mWidth;
		int mHeight;
	;

  • NativeTriangle2.cpp
#include "NativeTriangle2.h"

// 可以参考这篇讲解: https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
namespace NAMESPACE_NativeTriangle2 
	// 顶点着色器
	const char* VERTEX_SHADER_TRIANGLE =
		"#version 300 es                          \\n"
		"layout(location = 0) in vec4 a_position; \\n"
		"layout(location = 1) in vec4 a_color;    \\n"
		"out vec4 v_color;                        \\n"
		"void main()                              \\n"
		"                                        \\n"
		"   v_color = a_color;                    \\n"
		"   gl_Position = a_position;             \\n"
		"                                        \\n";

	// 片段着色器
	const char* FRAGMENT_SHADER_TRIANGLE =
		"#version 300 es                              \\n"
		"precision mediump float;                     \\n"
		"in vec4 v_color;                             \\n"
		"out vec4 o_fragColor;                        \\n"
		"void main()                                  \\n"
		"                                            \\n"
		"   o_fragColor = v_color;                    \\n"
		"                                            \\n";

	// 我们在OpenGL中指定的所有坐标都是3D坐标(x、y和z)
	// 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。
	// 我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。
	// https://learnopengl-cn.github.io/img/01/04/ndc.png

	// https://developer.android.com/guide/topics/graphics/opengl#kotlin
	// 在 OpenGL 中,形状的面是由三维空间中的三个或更多点定义的表面。
	// 一个包含三个或更多三维点(在 OpenGL 中被称为顶点)的集合具有一个正面和一个背面。
	// 如何知道哪一面为正面,哪一面为背面呢?这个问题问得好!答案与环绕(即您定义形状的点的方向)有关。
	// 查看图片 : https://developer.android.com/images/opengl/ccw-winding.png
	// 或者查看本地图片:Android_Java/Chapter_2/Hello_Triangle/ccw-winding.png
	// 在此示例中,三角形的点按照使它们沿逆时针方向绘制的顺序定义。
	// 这些坐标的绘制顺序定义了该形状的环绕方向。默认情况下,在 OpenGL 中,沿逆时针方向绘制的面为正面。
	// 因此您看到的是该形状的正面(根据 OpenGL 解释),而另一面是背面。
	//
	// 知道形状的哪一面为正面为何如此重要呢?
	// 答案与 OpenGL 的“面剔除”这一常用功能有关。
	// 面剔除是 OpenGL 环境的一个选项,它允许渲染管道忽略(不计算或不绘制)形状的背面,从而节省时间和内存并缩短处理周期:
	GLfloat vVertices[] = 
		// 逆时针 三个顶点
		0.0f, 0.5f, 0.0f,            // 上角
		-0.5f, -0.5f, 0.0f,          // 左下角
		0.5f, -0.5f, 0.0f            // 右下角
	;

	// 设置顶点的颜色值  这里设置成蓝色
	GLfloat color[4] =  0.0f, 0.0f, 1.0f, 1.0f ;


	NativeTriangle::NativeTriangle() 

	

	NativeTriangle::~NativeTriangle() 

	


	void NativeTriangle::create() 
		GLUtils::printGLInfo();

		mProgram = GLUtils::createProgram(&VERTEX_SHADER_TRIANGLE, &FRAGMENT_SHADER_TRIANGLE);
		if (!mProgram) 
			LOGD("Could not create program");
			return;
		
		// 设置清除颜色
		glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
	


	void NativeTriangle::draw() 
		// Clear the color buffer
		// 清除屏幕
		// 在OpenGL ES中,绘图中涉及多种缓冲区类型:颜色、深度、模板。
		// 这个例子,绘制三角形,只向颜色缓冲区中绘制图形。在每个帧的开始,我们用glClear函数清除颜色缓冲区
		// 缓冲区将用glClearColor指定的颜色清除。
		// 这个例子,我们调用了GLES30.glClearColor(1.0f, 1.0f, 1.0f, 0.0f); 因此屏幕清为白色。
		// 清除颜色应该由应用程序在调用颜色缓冲区的glClear之前设置。
		glClear(GL_COLOR_BUFFER_BIT);

		// Use the program object
		// 在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。
		// 当我们渲染一个物体时要使用着色器程序 , 将其设置为活动程序。这样就可以开始渲染了
		glUseProgram(mProgram);

		// Load the vertex data
		//  顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,
		//  它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。
		//  所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。

		//  我们的顶点缓冲数据会被解析为下面这样子:https://learnopengl-cn.github.io/img/01/04/vertex_attribute_pointer.png
		//   . 位置数据被储存为32位(4字节)浮点值。
		//   . 每个位置包含3个这样的值。
		//   . 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
		//   . 数据中第一个值在缓冲开始的位置。

		// 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
		// Load the vertex data

		//  指定通用顶点属性数组
		// 第一个参数指定我们要配置的顶点属性。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
		// 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
		// 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
		// 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
		// 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。我们设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。
		//      一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,
		//      (译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
		// 最后一个参数的类型是void*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。
		glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vVertices);

		// 现在我们已经定义了OpenGL该如何解释顶点数据,
		// 我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。
		glEnableVertexAttribArray(0);

		// Set the vertex color to red
		// 设置顶点的颜色值
		// 加载index指定的通用顶点属性,加载(x,y,z,w)
		// opengl各个坐标系理解与转换公式 https://blog.csdn.net/grace_yi/article/details/109341926
		// x,y,z,w:指的不是四维,其中w指的是缩放因子
		// X轴为水平方向,Y轴为垂直方向,X和Y相互垂直
		// Z轴同时垂直于X和Y轴。Z轴的实际意义代表着三维物体的深度
		glVertexAttrib4fv(1, color);
		// 相对于下面这句   这里设置成蓝色
//    glVertexAttrib4f(1,0.0,0.0,1.0f,1.0f);

		// glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。
		// 第二个参数指定了顶点数组的起始索引,我们这里填0。
		// 最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
		//        public static final int GL_POINTS                                  = 0x0000;
		//        public static final int GL_LINES                                   = 0x0001;
		//        public static final int GL_LINE_LOOP                               = 0x0002;
		//        public static final int GL_LINE_STRIP                              = 0x0003;
		//        public static final int GL_TRIANGLES                               = 0x0004;
		//        public static final int GL_TRIANGLE_STRIP                          = 0x0005;
		//        public static final int GL_TRIANGLE_FAN                            = 0x0006;
		glDrawArrays(GL_TRIANGLES, 0, 3);

		// 禁用 通用顶点属性数组
		glDisableVertexAttribArray(0);
	

	void NativeTriangle::change(int width, int height) 
		mWidth = width;
		mHeight = height;
		LOGD("change() width = %d , height = %d\\n", width, height);

		// Set the viewport
		以上是关于Modern OpenGL - 顶点数组、属性与绑定点(OpenGL 4.5+)的主要内容,如果未能解决你的问题,请参考以下文章

我的OpenGL学习进阶之旅顶点属性顶点数组

我的OpenGL学习进阶之旅顶点属性顶点数组

Modern OpenGL - 渲染矩体/矩形体/立方体/正方体/长方体

为啥禁用顶点属性数组零时OpenGL绘图失败?

如何声明和使用作为 OpenGL 中原始元素数组的顶点属性

初识OpenGL (-)VAO顶点数组对象