在OpenGL中,大量的数据在着色器中传递,数据通过Buffer和Texture两种形式组织。
1 缓存(Buffer)
OpenGL中Buffer为一段连续的内存空间,用names标识,在使用buffer时首先调用函数保存某个GLuint类型的name,将其绑定到GL的上下文,然后再为它分配内存空间,最后输入数据。
1.1 缓存生成以及内存空间分配
将缓存绑定至GL上下文时,需根据不同的缓存使用场景时指定缓存绑定目标。分配内存的GL函数如下;
void glBufferData(GLenum target,
GLsizeiptr size,
const GLvoid * data,
GLenum usage);
target参数指定了缓存区类型(The target parameter tells OpenGL which target the buffer you want to allocate storage for is bound to),只能根据使用场景选用OpenGL提供的枚举值。当存储顶点数据用于输入顶点着色器时,target可选用GL_ARRAY_BUFFER(但是OpenGL并不会真正的指定缓存的类型,该buffer后期可以指定其他类型target)。data为需要绑定数据的的首地址,当不需要立即缓存数据时可以指定NULL,usage指定了buffer的用途。
Buffer的使用枚举类型如下:
GL_STREAM_DRAW:代码输入,用于绘制。设置一次,并且很少使用。
GL_STREAM_READ:接受OpenGL输出,用于绘制。设置一次,并且很少使用。
GL_STREAM_COPY:接受OpenGL输出,用于绘制或者用于拷贝至图片。设置一次,很少使用。
GL_STATIC_DRAW:代码输入,用于绘制或者拷贝至图片。设置一次,经常使用。
GL_STATIC_READ:接受OpenGL输出,用于绘制。设置一次,代码经常查询。
GL_STATIC_COPY:接受OpenGL输出,用于绘制或者用于拷贝至图片。设置一次,经常使用。
GL_DYNAMIC_DRAW:代码经常更新其内容,用于绘制或者用于拷贝至图片,使用频率高。
GL_DYNAMIC_READ:OpenGL输出经常更新其内容,代码经常查询。
GL_DYNAMIC_COPY:OpenGL输出经常更新其内容,用于绘制或者用于拷贝至图片,使用频率高。
生成缓存的代码如下:
// The type used for names in OpenGL is GLuint
GLuint buffer;
// Generate a name for the buffer, 此处第一个参数为生成缓存个数,可以一次生成多个缓存
glGenBuffers(1, &buffer);
// Now bind it to the context using the GL_ARRAY_BUFFER binding point
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// Specify the amount of storage we want to use for the buffer,size单位为Byte,此处分配1MB空间
glBufferData(GL_ARRAY_BUFFER, 1024 * 1024, NULL, GL_STATIC_DRAW);
为分配好内存空间的缓存绑定数据三种方式,第一种是分配内存时同时指定数据。第二种代码如下,此处并未指定buffer的标识name,因为bindbuffer可以指定当前上下文活跃缓存,OpenGL默认为当前上下文中活跃的缓存输入数据。
// This is the data that we will place into the buffer object
static const float data[] = {
0.25, -0.25, 0.5, 1.0,
-0.25, -0.25, 0.5, 1.0,
0.25, 0.25, 0.5, 1.0
};
// Put the data into the buffer at offset zero
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(data), data);
第三种为缓存输入数据的方法如下,同理,此处并未指定buffer的标识name,因为bindbuffer可以指定当前上下文活跃缓存,OpenGL默认为当前上下文中活跃的缓存输入数据。
// This is the data that we will place into the buffer object
static const float data[] = {
0.25, -0.25, 0.5, 1.0,
-0.25, -0.25, 0.5, 1.0,
0.25, 0.25, 0.5, 1.0
};
// Get a pointer to the buffer’s data store
void * ptr = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY); // Copy our data into it...
memcpy(ptr, data, sizeof(data));
// Tell OpenGL that we’re done with the pointer
glUnmapBuffer(GL_ARRAY_BUFFER);
相交于前两种方法必须准备额外的内存空间读取文件中的数据,然后将其拷贝至缓存中,第三种方式可以直接获取缓存的地址,将文件读取到缓存中。
1.2 为缓存填充和拷贝数据
上一节为缓存输入数据的三个方法都会重写整个缓存区,当为缓存区输入常量时,使用glClearBufferSubData()
函数会更高效。
void glClearBufferSubData(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void * data);
该函数将data指向的数据转换为internalformat指定的格式,并将其复制到有offset和size(单位为bytes)指定的区域内。format描述数据颜色通道信息,其枚举值为GL_RED, GL_RG, GL_RGB, or GL_RGBA。type指定了数据的类型,其具体类型为。
(GL_BYTE -- GLchar) (GL_UNSIGNED_BYTE -- GLuchar) (GL_SHORT -- GLshort)
(GL_UNSIGNED_SHORT -- GLushort) (GL_INT -- GLint), (GL_UNSIGNED_INT -- GLuint)
(GL_FLOAT -- GLfloat) (GL_DOUBLE -- GLdouble)。
OpenGL中拷贝数据的函数如下:
void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset, GLintptr writeoffset, GLsizeiptr size);
由于一个Target在同一时间只能绑定一个buffer,因此不能在同一类型的buffer中拷贝数据。为此OpenGL为拷贝数据提供了两个专有target:GL_COPY_READ_BUFFER和GL_COPY_WRITE_BUFFER。此处需注意拷贝和读取数据的范围都不能超过buffer的边界,否则拷贝失败。
1.3 从缓存向顶点着色器传递数据
为顶点着色器传递的顶点数据称为顶点数组对象(VAO),其初始化方法如下:
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
函数glVertexAttribPointer
负责描述数据,其第一个参数用于指定在顶点着色器中的输入参数的索引,type指定数据变量类型,normalized描述数据的取值是否在0到1之间,stride描述了每个顶点的大小(单位为bytes),该参数可以设置为0使OpenGL根据size和type自动计算,pointer的名字取得很独特,但他真正描述的是顶点数据在缓存中的偏移量。函数glEnableVertexAttribArray
负责填充数据。
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);
void glEnableVertexAttribArray(GLuint index);
使用buffer为顶点着色器传递数据的代码如下:
// First, bind our buffer object to the GL_ARRAY_BUFFER binding
// The subsequent call to glVertexAttribPointer will reference this buffer glBindBuffer(GL_ARRAY_BUFFER, buffer);
// Now, describe the data to OpenGL, tell it where it is, and turn on // automatic vertex fetching for the specified attribute
glVertexAttribPointer(0, // Attribute 0
4, // Four components
GL_FLOAT, // Floating-point data
GL FALSE, // Not normalized
0, // (floating-point data never is tightly packed)
NULL); // Offset zero (NULL pointer)
glEnableVertexAttribArray(0);
其对应的顶点着色器代码如下:
#version 410 core
layout (location = 0) in vec4 position;
void main(void) {
gl_Position = position;
}
在数据输入完毕后,可以调用(通常不调用)glDisableVertexAttribArray()
禁用属性。
1.4 顶点着色器多属性输入
对于接收多属性的顶点着色器如下,当其被链接至program时可以使用函数GLint glGetAttribLocation(GLuint program, const GLchar * name);
获取属性位置。该函数中name传入着色器中同名字符串,如“position”的返回值为0,“color”的返回值为1。如果传入为定义的字符串,此时返回值为-1。如果在顶点着色器中未为输入属性指定位置,在调用该函数时OpenGL会自动为其分配一个位置并将其返回。
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
顶点着色器多属性输入的方式分为分离属性(separate attributes)和交错属性(interleaved attributes)。
分离属性指将数据放置在两个缓存中,分别通过glVertexAttribPointer
等函数将数据传入着色器中,代码如下。
GLuint buffer[2];
static const GLfloat positions[] = { ... };
static const GLfloat colors[] = { ... };
// Get names for two buffers
glGenBuffers(2, &buffers);
// Bind the first and initialize it
glBindBuffer(GL_ARRAY_BUFFER, buffer[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(0);
// Bind the second and initialize it
glBindBuffer(GL_ARRAY_BUFFER, buffer[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
glEnableVertexAttribArray(1);
交错属性指将顶点着色器多个属性的数据都存储在同一个缓存中,多次调用函数glVertexAttribPointer
并指定缓存地质偏移量为多个属性传递数据,其代码如下。该函数最后一个参数表示该属性数据起点在和每个顶点数据起始内存地址之间的偏移值。因此数据的组织形式必须是以顶点为单位,即将单个顶点的所有数据描述完后再继续描述下一个顶点。
// 定义保存顶点数据的结构体如下
struct vertex {
// Position float x; float y; float z;
// Color float r; float g; float b;
}
GLuint buffer;
static const vertex vertices[] = { ... };
// Allocate and initialize a buffer object
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// Set up two vertex attributes - first positions
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(vertex), (void *)offsetof(vertex, x));
glEnableVertexAttribArray(0);
// Now colors,offsetof为c语言宏,用于计算结构体中成员的内存地址偏移位,单位为bytes
glVertexAttribPointer(1, 3, GL FLOAT, GL FALSE, sizeof(vertex), (void *)offsetof(vertex, r));
glEnableVertexAttribArray(1);
1.5 从文件中加载数据
在实际的编程中模型是都很复杂,其中的顶点几乎不能通过代码定义。模型通常都由3D艺术家在Blender、3DS Max或者Maya等软件生成,这些软件可以将顶点数据导出在文件中,但是不同软件的导出文件格式都不相同,通常使用Assimp库导入模型文件。Assimp能够导入很多种不同的模型文件格式(并也能够导出部分的格式),它会将所有的模型数据加载至Assimp的通用数据结构中。其具体教程见链接,此处由于是超级圣杯这本书的读书笔记,因此使用其提供的文件结构SBM。
总的来说,想要读取某种格式的模型文件,通常需要了解其数据组织结构,使用c语言标注库<stdio.h>中的FILE相关函数读取二进制文件。超级圣杯源代码中提供了sb6::object类用于读取sbm格式模型文件,SBM文件结构简介如下,具体见原著。
1.5.1 文件头
SBM文件有一个文件头和多个数据块及原始数据三部分组成,每个数据块又由一个数据头和数据体组成。其中数据都是以小端模式保存,并且数据块只是描述数据,真正的顶点数据都保存在原始数据区域内。其中文件头结构体定义如下。