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中的偏移),随后执行绘制

最终运行效果如下:

image

posted @ 2021-05-09 19:41  图袋鼠  阅读(439)  评论(0编辑  收藏  举报