顶点数据也称作顶点属性,指定每个顶点的数据。这种逐顶点数据可以为每个顶点指定,也可以用于所有顶点的常量。
例如,如果你想要绘制固定颜色的三角形(如下图,假定颜色为黑色),可以指定一个常量值,用于三角形的全部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
指定的通用属性。
glVertexAttrib1f
和glVertexAttrib1fv
函数在通用顶点属性中加载(x, 0.0, 0.0, 1.0)
glVertexAttrib2f
和glVertexAttrib2fv
函数在通用顶点属性中加载(x, y, 0.0, 1.0)
glVertexAttrib3f
和glVertexAttrib3fv
函数在通用顶点属性中加载(x, y, z, 1.0)
glVertexAttrib4f
和glVertexAttrib4fv
函数在通用顶点属性中加载(x, y, z, w)
在实践中,常量顶点属性提供与使用标量/向量统一变量等价的功能,两者都是可以接受的选择。
1.2 顶点数组
顶点数组指定每个顶点的属性,是保存在应用程序地址空间(OpenGL ES 称为客户空间) 的缓冲区。
它们作为顶点缓冲区对象的基础,提供指定顶点属性数据的一种高效、灵活的手段。
顶点数组用glVertexAttribPointer
或 glVertexAttribIPointer
函数指定。
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 中的实现方式。
glEnableVertexAttribArray
和glDisableVertexAttribArray
命令分别用于启用和禁用通用顶点属性数组。
如果某个通用属性索引的顶点属性数组被禁用,则将使用为该索引指定的常量顶点属性数组。
void glDisableVertexAttribArray (GLuint index);
void glEnableVertexAttribArray (GLuint index);
- index
指定通用顶点属性索引。这个值的范围从0到支持的最大顶点属性数量减1。
1.7.1 例3 使用常量和顶点数组属性
例3 说明如何绘制一个三角形,该三角形的一个顶点属性是常量,其他属性用顶点数组指定。
下面的代码可以在 GitHub - ouyangpeng/OpenGLESDemo: Android OpenGL ES 3.0 练手学习Demo 中去查看源代码
- 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
// 通知OpenGL ES 用于绘制的2D渲染表面的原点、宽度和高度。
// 在OpenGL ES 中,视口(Viewport) 定义所有OpenGL ES 渲染操作最终显示的2D矩形
// 视口(Viewport) 由原点坐标(x,y)和宽度(width) 、高度(height)定义。
glViewport(0, 0, mWidth, mHeight);
}
}
// ====================================================================
NAMESPACE_NativeTriangle2::NativeTriangle* nativeTriangle2;
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_openglesdemo_triangle_HelloTriangle2NativeRenderer_nativeSurfaceCreate(
JNIEnv * env, jobject thiz) {
if (nativeTriangle2) {
delete nativeTriangle2;
nativeTriangle2 = nullptr;
}
nativeTriangle2 = new NAMESPACE_NativeTriangle2::NativeTriangle();
nativeTriangle2->create();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_openglesdemo_triangle_HelloTriangle2NativeRenderer_nativeSurfaceChange(
JNIEnv * env, jobject thiz, jint width, jint height) {
if (nativeTriangle2 != nullptr) {
nativeTriangle2->change(width, height);
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_oyp_openglesdemo_triangle_HelloTriangle2NativeRenderer_nativeDrawFrame(
JNIEnv * env, jobject thiz) {
if (nativeTriangle2 != nullptr) {
nativeTriangle2->draw();
}
}
代码示例中,
- 使用的顶点属性 color 是一个常量,用
glVertexAttrib4fv(1, color);
指定,不启用顶点属性数组1 - vVertices属性用
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, vVertices);
以一个顶点数组指定,并用glEnableVertexAttribArray(0);
启用数组0. - color值对于所绘制的三角形的所有顶点均相同,而vVertices属性对于三角形的各个顶点可以不同。
二 、在顶点着色器中声明顶点属性变量
我们已经知道顶点属性的定义,并且考虑了如何在OpenGL ES中指定顶点属性,现在讨论如何在顶点着色器中声明顶点属性。
在顶点着色器中,变量通过使用in限定符
声明为顶点属性。
属性变量也可以选择包含一个布局限定符
,提供属性索引。
下面是几个顶点属性声明的示例:
layout(location = 0) in vec4 a_position;
layout(location = 1) in vec4 a_color;
layout(location = 2) in vec2 a_texcoord;
layout(location = 3) in vec3 a_normal;
in限定符
只能用于以下数据类型:
- float、vec2、vec3、vec4、
- int、ivec2、ivec3、ivec4、
- uint、uvec2、uvec3、uvec4
- mat2 、mat2x3 、mat2x4 、mat3x2 、mat3、mat3x4 、mat4x2 、mat4x3 、mat4
属性变量不能声明为数组或者结构。
下面是无效顶点属性声明的例子,应该产生一个编译错误:
in foo_t a_A; //foo_t is a structure
int vec4 a_B[10];
在顶点着色器中声明为顶点属性的变量是只读变量,不能修改。
下面代码将导致编译错误:
in vec4 a_pos;
uniform vec4 u_v;
void main()
{
a_pos = u_v; // cannot assign to a_pos as it is read-only
}
2.1 使用glGetProgramiv
命令查询命令活动顶点属性数量
属性可以在顶点着色器内部声明,但是如果没有使用,就不会被认为是活动属性,从而不会被计入限制。
如果在顶点着色器中使用的属性数量大于GL_MAX_VERTEX_ATTRIBS,这个顶点着色器将无法链接。
一旦程序链接成功,那么我们就需要找出连接到该程序的顶点着色器使用的活动顶点属性数量。
注意,这一步骤只在你对属性不使用输入布局限定符时才有必要。在OpenGL ES 3.0中,建议使用布局限定符 ,这样你就没有必要事后查询这一信息。
下面代码展示了如何获得活动顶点属性数量:
在博客【我的OpenGL学习进阶之旅】程序对象 介绍过
void glGetProgramiv (GLuint program, GLenum pname, GLint *params);
2.2 使用glGetActiveAttrib
命令查询活动顶点属性列表和它们的数据类型
程序使用的活动顶点属性列表和它们的数据类型可以用glGetActiveAttrib
命令查询。
void glGetActiveAttrib (GLuint program, GLuint index,
GLsizei bufSize, GLsizei *length,
GLint *size, GLenum *type,
GLchar *name);
参数说明:
-
program
前面成功链接的程序对象名称 -
index
指定需要查询的顶点属性,其值为0到GL_ACTIVIE_ATTRIBUTES -1
。GL_ACTIVIE_ATTRIBUTES
的值用glGetProgramiv
确定。 -
bufSize
指定可以写入name
的最大字符数,包括Null终止符 -
length
返回写入name
的字符数,如果length
不为NULL,则不含Null终止符 -
type
返回属性类型,有效值为:- GL_FLOAT、GL_FLOAT_VEC2、GL_FLOAT_VEC3、GL_FLOAT_VEC4、
- GL_FLOAT_MAT2、GL_FLOAT_MAT3、GL_FLOAT_MAT4、
- GL_FLOAT_MAT2x3、GL_FLOAT_MAT2x4、
- GL_FLOAT_MAT3x2、GL_FLOAT_MAT3x4、
- GL_FLOAT_MAT4x2、GL_FLOAT_MAT4x3
- GL_INT、GL_INT_VEC2、GL_INT_VEC3、GL_INT_VEC4、
- GL_UNSIGNED_INT、GL_UNSIGNED_INT_VEC2、
- GL_UNSIGNED_INT_VEC3、GL_UNSIGNED_INT_VEC4
-
size
返回属性大小。这以type
返回的类型单元格数量指定。如果数量不是一个数组,则size
总数为1。如果变量是一个数组,则size
返回数组的大小。 -
name
顶点着色器中声明的属性变量名称
三、将顶点属性绑定到顶点着色器中的属性变量
前面我们讨论了顶点着色器中,顶点属性变量由in限定符
指定,活动属性数量可以用glGetProgramiv
查询,程序中的活动属性列表可以用glGetActiveAttrib
查询。
我们还讨论了如何使用 glVertexAttrib*
和 glVertexAtrtribPointer
命令指定常量或者逐顶点(顶点数组)值。
现在,我们考虑如何将这个通用属性索引映射到顶点着色器中声明的对于属性变量。这种映射使对应的顶点数据可以读入顶点着色器中正确的顶点属性变量。
下图描述了 指定通用顶点属性和绑定到顶点着色器中的属性名称的方法。
3.1 将通用顶点属性索引映射到顶点着色器中的一个属性变量名称的3种方法
在OpenGL ES 3.0中,可以使用3种方法将通用顶点属性索引映射到顶点着色器中的一个属性变量名称。
这些方法可以分为如下几类:
- 索引可以在顶点着色器源码中用
layout( location = N )
限定符指定(推荐)。 - OpenGL ES 3.0将通用顶点属性索引绑定到属性名称。
- 应用程序可以将顶点属性索引绑定到属性名称。
将属性绑定到一个位置最简单的方法是简单地使用 layout( location = N )
限定符,这种方法需要的代码最少。
但是,在某些情况下,其他两个选项可能更合适。
3.2 glBindAttribLocation
命令
glBindAttribLocation
命令可以用于将通用顶点属性索引绑定到顶点着色器中的一个属性变量。这种绑定在下一次程序链接时失效,不会改变当前链接的程序中使用的绑定。
void glBindAttribLocation (GLuint program, GLuint index, const GLchar *name);
参数说明:
- program
程序对象名称 - index
通用顶点属性索引 - name
属性变量名称
如果之前绑定了name
,则它所指定的绑定被索引代替。glBindAttribLocation
命令甚至可以在顶点着色器连接到程序对象之前调用,因此,这个调用可以用于绑定任何属性名称。不存在的属性名称或者连接到程序对象的顶点着色器中不活动的属性将被忽略。
另一个选项是让OpenGL ES 3.0 将属性变量名称绑定到一个通用顶点属性索引。这种绑定在程序链接时进行。
在链接阶段,OpenGL ES 3.0 实现为每个属性变量执行如下操作:
对于每个属性变量,检查是否已经通过glBindAttribLocation
命令指定了绑定。如果指定了一个绑定,则使用指定的对于属性索引。否则,OpenGL ES实现将分配一个通用顶点属性索引。
这种分配特定于实现。也就是说,在一个OpenGL ES 3.0 实现中和在另一个实现中可能不同。
3.2 glGetAttribLocation命令
应用程序可以使用glGetAttribLocation
命令查询分配的绑定
GLint glGetAttribLocation (GLuint program, const GLchar *name);
参数说明:
-
program
程序对象名称 -
name
属性变量名称
glGetAttribLocation
命令返回program
定义的程序对象最后一次链接时绑定到属性变量name
的通用属性索引。
如果name
不是一个活动属性变量,或者program
不是一个有效的程序对象,或者没有链接成功,则返回-1
,表示无效的属性索引。