OpenGL ES入门 - 顶点缓冲和顶点属性 Vertex Buffers and Attributes
1. Things OpenGL Can Render
图中展示了OpenGL 能够渲染三种类型的物体:点、线和三角形
2. Everything's a Triangle
虽然能够渲染三种类型,但是最终复杂的图形通常由三角形构成,图中的矩形和圣诞树都是由三角形构成的:
接下来我们尝试理解一个简单的三角形是如何完成的
3. Storing Vertex Info
在我们的屏幕中,坐标构成如上图。即屏幕中间为(0,0),范围从-1 到1。所以首先我们为坐标创建一个结构体:
typedef struct {
GLFloat position[3]; // 分别代表x, y, z
}TestVertex;
结构体实例如下:
const static TestVertex vertices[] = {
{{-1.0, -1.0, 0}}, // 左下
{{1.0, -1.0, 0}}, // 右下
{{0, 0, 0}}, // 上
}
4. Sending Vertex Info to GPU
需要经过三个方法:
glGenBuffer() // 在GPU中创建一个VertexBuffer
glBindBuffer() // 绑定并激活VertexBuffer
glBufferData() // 将CPU中数据传输至VertexBuffer,供后续处理
5. Drawing Geometry
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer) // 将VertexBuffer至ArrayBuffer
glDrawArrays(GL_TRIANGLES, 0, 3) // 绘制真正发生的地方
到这里绘制就完成了,但是GPU是如何通过顶点来绘制图像呢,其背后的原理就涉及到了shader
6. Shader
Shader是通过OpenGL ES Shading Language(GLSL)来操作的,这是一个C-like语言。
Shader包括两种类型:
- Vertex Shader:输入顶点,并为顶点输出最终的位置信息
- Fragment Shader:输入像素,输出最终颜色信息
6.1 Vertex Shader
vertex shader的输入是顶点信息,随后通过一系列的transform,输出最终的位置信息,这里为了简单,我们并不做复杂的transform,他的GLSL代码如下:
attribute vec4 a_Positon;
void main(void) {
gl_position = a_Position;
}
6.2 Fragment Shader
fragment shader的输入通常来自于vertex shader,这里为了简化,我们免去了输入,直接输出像素的颜色
void main(void) {
gl_FragColor = vec4(1, 1, 1, 1); // 将一切像素输出为白色
}
7 Demo:Rendering a Triangle
7.1 Add Helper Code
作者提供了一些resource来封装对shader的操作,以及GLSL的实现。需要将这些代码引入工程,可供我们使用+学习。下载地址在此
7.2 Setup vertex buffer
viewDidLoad代码如下
- (void)viewDidLoad
{
[super viewDidLoad];
GLKView *view = (GLKView *)self.view;
view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:view.context];
[self setupShader];
[self setupVertexBuffer];
}
可以看到,viewDidLoad主要做了两件事,就是setupShader和setupVertexBuffer,那么我们一起来看看内部的逻辑:
- (void)setupVertexBuffer {
const static RWTVertex vertices[] = {
{{-1.0, -1.0, 0}}, // 左下
{{1.0, -1.0, 0}}, // 右下
{{0, 0, 0}}, // 上方
};
// 按照上面说的将CPU数据传给GPU的三个步骤
glGenBuffers(1, &_vertexBuffer); // 生成
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer); // 绑定
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 绘制
}
setupVertexBuffer主要功能就是初始化顶点,随后将顶点从CPU传输至GPU
- (void)setupShader {
_shader = [[RWTBaseEffect alloc] initWithVertexShader:@"RWTSimpleVertex.glsl" fragmentShader:@"RWTSimpleFragment.glsl"];
}
setupShader主要调用了作者提供的初始化函数来对shader进行初始化,稍后我们对初始化函数再进行分析
7.3 Enable/disable vertex attributes
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
[_shader prepareToDraw];
// enable vertexAttribute
glEnableVertexAttribArray(RWTVertexAttribPosition);
// 指定顶点属性在vertex buffer中的偏移,告诉GPU该如何取顶点数据
glVertexAttribPointer(RWTVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(RWTVertex), (const GLvoid *) offsetof(RWTVertex, Position));
glBindBuffer(GL_ARRAY_BUFFER, _vertexBuffer);
glDrawArrays(GL_TRIANGLES, 0, 3);
// disable
glDisableVertexAttribArray(RWTVertexAttribPosition);
}
7.4 Helper Code分析
接下来看看作者提供的初始化函数的内部,实际内容就是对shader进行compile的操作
- (void)compileVertexShader:(NSString *)vertexShader
fragmentShader:(NSString *)fragmentShader {
// 载入事先写好的GLSL代码,并编译shader
GLuint vertexShaderName = [self compileShader:vertexShader
withType:GL_VERTEX_SHADER];
GLuint fragmentShaderName = [self compileShader:fragmentShader
withType:GL_FRAGMENT_SHADER];
// 创建glProgram,shader导入
_programHandle = glCreateProgram();
glAttachShader(_programHandle, vertexShaderName);
glAttachShader(_programHandle, fragmentShaderName);
glBindAttribLocation(_programHandle, RWTVertexAttribPosition, "a_Position");
// 将glProgram与shader绑定
glLinkProgram(_programHandle);
GLint linkSuccess;
glGetProgramiv(_programHandle, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(_programHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
exit(1);
}
}
看到作者是通过compileShader函数对shader进行compile的,我们看看这个函数内部:
- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
// 载入事先写好的GLSL
NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName ofType:nil];
NSError* error;
NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSLog(@"Error loading shader: %@", error.localizedDescription);
exit(1);
}
// 创建shader
GLuint shaderHandle = glCreateShader(shaderType);
const char * shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = [shaderString length];
// 将GLSL导入shader中
glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
// 编译
glCompileShader(shaderHandle);
GLint compileSuccess;
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(shaderHandle, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSLog(@"%@", messageString);
exit(1);
}
return shaderHandle;
}
整体代码地址可以到作者主页去下载
8. 总结
我们来总结下绘制三角形都需要哪些步骤:
- 配置GLContext
- 初始化顶点,将其从CPU传输至CPU
- 通过GLSL编译shader,并绑定只GLProgram上
- 在glkView回调函数中,渲染背景色,enable vertex attribute(配置顶点在vertexbuffer中的偏移),随后执行绘制
最终运行效果如下: