首页 关于 微信公众号
欢迎关注我的微信公众号

OpenGL入门

概述

OpenGL是一个低级编程的API,它可以实现2D和3D图形编程。cocos2d,sparrow,corona,unity 这些框架,你会发现其实它们都是基于OpenGL上创建的。

本篇教程是入门级教程,主要讲述在使用OpenGL时的一些重要概念,以及对一些示例程序进行讲解。

重要概念

着色器

在OpenGL ES2.0 的世界,在场景中渲染任何一种几何图形,你都需要创建两个称之为“着色器”的小程序。着色器由一个类似C的语言编写- GLSL 。

有两种类型的着色器,它们分别是:顶点着色器和片段着色器。

示例程序

HelloOpenGL

1)创建工程并添加OpenGLView类,继承自UIView

如下是 OpenGLView.h

#import <UIKit/UIKit.h>
#import <OpenGLES/ES2/gl.h>
#import <OpenGLES/ES2/glext.h>
#import <QuartzCore/QuartzCore.h>

@interface OpenGLView : UIView
{
    CAEAGLLayer* _eaglLayer;
    EAGLContext* _context;
    GLuint _colorRenderBuffer;
}

@end

2)设置 layer class 为 CAEAGLLayer

+ (Class)layerClass {
    return [CAEAGLLayer class];
}

想要显示OpenGL的内容,你需要把它缺省的layer设置为一个特殊的layer。(CAEAGLLayer)。这里通过直接复写layerClass的方法。

3) 设置layer为不透明

- (void)setupLayer {
    _eaglLayer = (CAEAGLLayer*) self.layer;
    _eaglLayer.opaque = YES;
}

因为缺省的话,CALayer是透明的。而透明的层对性能负荷很大,特别是OpenGL的层。

4)创建OpenGL context

- (void)setupContext {   
    EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
    _context = [[EAGLContext alloc] initWithAPI:api];
    if (!_context) {
        NSLog(@"Failed to initialize OpenGLES 2.0 context");
        exit(1);
    }
 
    if (![EAGLContext setCurrentContext:_context]) {
        NSLog(@"Failed to set current OpenGL context");
        exit(1);
    }
}

无论你要OpenGL帮你实现什么,总需要这个 EAGLContext ,EAGLContext管理所有通过OpenGL进行draw的信息。这个与Core Graphics context类似。

5) 创建render buffer(渲染缓冲区)

- (void)setupRenderBuffer {
    glGenRenderbuffers(1, &_colorRenderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);        
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];    
}

Render buffer 是OpenGL的一个对象,用于存放渲染过的图像。有时候你会发现render buffer会作为一个color buffer被引用,因为本质上它就是存放用于显示的颜色。

创建render buffer的三步:

6)创建一个 frame buffer(帧缓冲区)

- (void)setupFrameBuffer {    
    GLuint framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
        GL_RENDERBUFFER, _colorRenderBuffer);
 }

Frame buffer也是OpenGL的对象,它包含了前面提到的render buffer,以及其它后面会讲到的诸如:depth buffer、stencil buffer 和 accumulation buffer。

前两步创建frame buffer的动作跟创建render buffer的动作很类似。(反正也是用一个glBind什么的)

而最后一步 glFramebufferRenderbuffer 这个才有点新意。它让你把前面创建的buffer render依附在frame buffer的GL_COLOR_ATTACHMENT0位置上。

7)清理屏幕

- (void)render {
    glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    [_context presentRenderbuffer:GL_RENDERBUFFER];
}

 为了尽快在屏幕上显示一些什么,在我们和那些 vertexes、shaders打交道之前,把屏幕清理一下,显示另一个颜色吧。(RGB 0, 104, 55,绿色吧)

这里每个RGB色的范围是0~1,所以每个要除一下255.

下面解析一下每一步动作:

8)串联以上动作,修改OpenGLView.m

// Replace initWithFrame with this
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {        
        [self setupLayer];        
        [self setupContext];                
        [self setupRenderBuffer];        
        [self setupFrameBuffer];                
        [self render];        
    }
    return self;
}

9) 在 ViewController 中引入 OpenGLView

#import "ViewController.h"
#import "OpenGLView.h"
@interface ViewController ()
@property (nonatomic,strong) OpenGLView *glView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    _glView = [[OpenGLView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:_glView];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
@end

10) 运行

如果以上步骤一切顺利的话,你将会得到纯颜色(绿色)界面。

添加着色器

1)添加顶点着色器

创建一个空文件,命名为 SimpleVertex.gls

attribute vec4 Position; // 1
attribute vec4 SourceColor; // 2
 
varying vec4 DestinationColor; // 3
 
void main(void) { // 4
    DestinationColor = SourceColor; // 5
    gl_Position = Position; // 6
}

1 “attribute”声明了这个shader会接受一个传入变量,这个变量名为“Position”。在后面的代码中,你会用它来传入顶点的位置数据。这个变量的类型是“vec4”,表示这是一个由4部分组成的矢量。

2 与上面同理,这里是传入顶点的颜色变量。

3 这个变量没有“attribute”的关键字。表明它是一个传出变量,它就是会传入片段着色器的参数。“varying”关键字表示,依据顶点的颜色,平滑计算出顶点之间每个像素的颜色。

4 每个shader都从main开始– 跟C一样嘛。

5 设置目标颜色 = 传入变量:SourceColor

6 gl_Position 是一个内建的传出变量。这是一个在 vertex shader中必须设置的变量。这里我们直接把gl_Position = Position; 没有做任何逻辑运算。

2)添加片段着色器

varying lowp vec4 DestinationColor; // 1
 
void main(void) { // 2
    gl_FragColor = DestinationColor; // 3
}

1 这是从vertex shader中传入的变量,这里和vertex shader定义的一致。而额外加了一个关键字:lowp。在fragment shader中,必须给出一个计算的精度。出于性能考虑,总使用最低精度是一个好习惯。这里就是设置成最低的精度。如果你需要,也可以设置成medp或者highp.

2 也是从main开始嘛

3 正如你在vertex shader中必须设置gl_Position, 在fragment shader中必须设置gl_FragColor.

3)编译顶点着色器和片段着色器

目前为止,xcode仅仅会把这两个文件copy到application bundle中。我们还需要在运行时编译和运行这些shader。你可能会感到诧异。为什么要在app运行时编译代码?这样做的好处是,我们的着色器不用依赖于某种图形芯片。(这样才可以跨平台嘛)

- (GLuint)compileShader:(NSString*)shaderName withType:(GLenum)shaderType {
 
    // 1
    NSString* shaderPath = [[NSBundle mainBundle] pathForResource:shaderName 
        ofType:@"glsl"];
    NSError* error;
    NSString* shaderString = [NSString stringWithContentsOfFile:shaderPath 
        encoding:NSUTF8StringEncoding error:&error];
    if (!shaderString) {
        NSLog(@"Error loading shader: %@", error.localizedDescription);
        exit(1);
    }
 
    // 2
    GLuint shaderHandle = glCreateShader(shaderType);    
 
    // 3
constchar* shaderStringUTF8 = [shaderString UTF8String];    
    int shaderStringLength = [shaderString length];
    glShaderSource(shaderHandle, 1, &shaderStringUTF8, &shaderStringLength);
 
    // 4
    glCompileShader(shaderHandle);
 
    // 5
    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;
 
}

解析:

1 这是一个UIKit编程的标准用法,就是在NSBundle中查找某个文件。大家应该熟悉了吧。

2 调用 glCreateShader来创建一个代表shader 的OpenGL对象。这时你必须告诉OpenGL,你想创建 fragment shader还是vertex shader。所以便有了这个参数:shaderType

3 调用glShaderSource ,让OpenGL获取到这个shader的源代码。(就是我们写的那个)这里我们还把NSString转换成C-string

4 最后,调用glCompileShader 在运行时编译shader

5 大家都是程序员,有程序的地方就会有fail。有程序员的地方必然会有debug。如果编译失败了,我们必须一些信息来找出问题原因。 glGetShaderiv 和 glGetShaderInfoLog 会把error信息输出到屏幕。(然后退出)

关联两个着色器

- (void)compileShaders {
 
    // 1
    GLuint vertexShader = [self compileShader:@"SimpleVertex" 
        withType:GL_VERTEX_SHADER];
    GLuint fragmentShader = [self compileShader:@"SimpleFragment" 
        withType:GL_FRAGMENT_SHADER];
 
    // 2
    GLuint programHandle = glCreateProgram();
    glAttachShader(programHandle, vertexShader);
    glAttachShader(programHandle, fragmentShader);
    glLinkProgram(programHandle);
 
    // 3
    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);
    }
 
    // 4
    glUseProgram(programHandle);
 
    // 5
    _positionSlot = glGetAttribLocation(programHandle, "Position");
    _colorSlot = glGetAttribLocation(programHandle, "SourceColor");
    glEnableVertexAttribArray(_positionSlot);
    glEnableVertexAttribArray(_colorSlot);
}

下面是解析:

  1 用来调用你刚刚写的动态编译方法,分别编译了vertex shader 和 fragment shader

  2 调用了glCreateProgram glAttachShader glLinkProgram 连接 vertex 和 fragment shader成一个完整的program。

  3 调用 glGetProgramiv lglGetProgramInfoLog 来检查是否有error,并输出信息。

  4 调用 glUseProgram 让OpenGL真正执行你的program

  5 最后,调用 glGetAttribLocation 来获取指向 vertex shader传入变量的指针。以后就可以通过这写指针来使用了。还有调用 glEnableVertexAttribArray来启用这些数据。(因为默认是 disabled的。)    最后还有两步:

 1 在 initWithFrame方法里,在调用render之前要加入这个:

[self compileShaders];

 2 在@interface in OpenGLView.h 中添加两个变量:

GLuint _positionSlot;
GLuint _colorSlot;

如果你仍能正常地看到之前那个绿色的屏幕,就证明你前面写的代码都很好地工作了。

Blog

Opinion

Project