美文网首页
OpenGL ES 入门之iOS平台

OpenGL ES 入门之iOS平台

作者: 风轻知道 | 来源:发表于2017-12-27 18:15 被阅读119次

    本文仅适合零基础入门OpenGL ES的同学

    参考自:https://www.raywenderlich.com/3664/opengl-tutorial-for-ios-opengl-es-2-0

    简介

    也许你曾使用其他的游戏引擎,比如Cocos2D,Sparrow, Corona或者是Unity,这些都是基于OpenGL之上的。为什么程序员们喜欢这些框架,因为OpenGL太难学了。写这篇入门教程的原因就是缩短OpenGL 初学者的学习曲线。

    在这片教程里,你可以动手通过opengl es创建一个hello world程序,显示一些几何图形。

    在过程中,你将会学到:

    1. 如何通过scratch得到一个基本的opengl app
    2. 如何编译和运行基于vertex和fragment的shader文件的 app
    3. 如何渲染一个立方体,通过vertex 缓存对象
    4. 如何应用投影和模型视图转换
    5. 如何通过深度测试渲染一个3D物体闲话少说,我们开始学习OpenGL ES!OpenGL ES 1.0 vs OpenGL ES 2.0
    • 你要知道OpenGL ES只有两个版本,V1和V2,他们完全不同
    • V1.0使用的是固态管线,一种方式使用内部函数实现设置灯光,顶点坐标,颜色,摄像头等等。
    • V2.0使用的是可编程管线,V1.0的内嵌函数都没了,你必须自己实现所有东西,你想要的。
    • 哦天哪!你可能会想,为什么我要用V2.0,我要做这么多额外的工作!虽然V2需要做额外的工作,但是,V2可以实现更多V1无法实现的更酷的效果,比如toon shader(卡通渲染).说到toon shader,不得不说一下shader。Shader

    着色器

    是一段在GPU上执行的针对3D对象进行操作的程序。应用于计算机图形学领域,指一组供计算机图形资源在执行渲染任务时使用的指令,用于计算图像的颜色和明暗。但近来,它也能用于处理一些特殊效果,或者视频后处理。通俗的说,着色器告诉电脑如何用特有的一种方法去绘制物体。程序员将着色器应用于图形处理器GPU的可编程流水线,来实现三维应用程序。这样的图形处理器有别于传统的固定流水线处理器,为GPU编程带来更高的灵活性和适应性。以前固有的流水线只能进行一些几何变换和像素灰度计算。现在可编程流水线还能处理所有像素、顶点、纹理的位置、色调、饱和度、明度、对比度并实时的绘制图像。着色器还能产生如模糊、高光、有体积光源、失焦、卡通渲染、色调分离、畸变、凹凸贴图、边缘检测、运动检测等效果。准备开始虽然Xcode准备了一个opengles的模板,我认为它会对初学者造成困惑,因为自动生成了很多令人困惑的代码。还要努力去理解它怎么工作的。我认为全部用scratch写代码相对容易,所以你能理解所有的是怎么协作的,我们现在开始。

    首先创建一个测试View,从UIView派生,layerClass指定是OpenGL的layer。
    然后在一个UIViewController上添加这个View
    代码如下:

    - (void)viewDidLoad {
            [super viewDidLoad];
    
            YMOpenGLView* ogv = [[YMOpenGLView alloc] initWithFrame:self.view.bounds];
            [self.view addSubview:ogv];
    }
    

    运行之后,会是一个被填充绿色的界面。
    在OpenGL ES里,渲染任何一个几何图形到场景中,你必须创建两个小的程序,叫做shaders。
    shaders是通过类似C语言的GLSL写成。不用担心学习这块,我们下面开始。

    shaders 包括两种类型:
    1、Vertex shaders: 在你的场景里每个定点都会调用一次的程序。所以如果你打算在场景里渲染一个正方形,每个转角都有一个定点,那么将会发生四次调用。它的工作是执行一些计算,比如光照、几何变换等等。找到定点的坐标,并传递一些数据给另外一个shader (Fragment shader)。
    2、Fragment shader: 在你的场景里每个像素都会调用一次的程序。所以如果你同样渲染一个正方形,它将被正方形覆盖的每个像素分别调用一次。Fragment shaders 也能执行一些光照计算等操作,但它更重要的工作是设置每个像素的颜色。

    为了更好的理解,结合示例说明。

    // 这是一个vertex shader
    
    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]这个关键字声明将要传入一个输入参数叫做Position。稍后,你将要写一些代码传入这个变量。将被用来表示顶点的坐标。注意类型是vec4,表示四分量向量。
    2、声明了第二个输入变量,定义的是顶点的颜色。
    3、声明了另外一个变量,但它不包含attribute关键字,所以它是一个输出的变量,输出给fragment shader.但它包含[varying]关键字,是一种幻想的方式在说:“我要告诉你特定顶点的值,但当你要找出特定像素的值的时候,通过附近顶点的平滑值获得(附近顶点的平均值)”。所以基本上,你可以为每个顶点设定不同的颜色,实现一个平滑整洁的渐变效果!一会儿实现这个效果。
    4、每个shader文件(程序)以一个入口函数开始,类似C!
    5、设置顶点的destination color等于source color,让OpenGL 做差值。
    6、有个已有的顶点在vertex shader里必须设置,它叫gl_Position,这里值不做变化。

    好了,这就是一个简单的vertex shader!让我们继续添加 fragment shader。

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

    这个非常短,但是我们要一行行解释:

    1、这是一个输入变量来自vertex shader。它和vertex shader 中定义的是一样的,例外的是它有了一个额外的关键字定义[lowp],当你在fragment shader中设定变量值的时候,你需要给它一个精度。一个很好的经验法则是尽量使用最低的精度,以获得性能奖金(更佳的性能)。我们设置了最低,但是也可以设置中和高在这里,它们是medp and highp,如果你需要的话。
    2、就像vertex shader一样,fragment shader也需要一个main作为入口函数。
    3、就像在vertex shader中必须要设置gl_Position一样,你必须设置gl_FragColor在这里。这里做了简单的设置。

    好了,还不错吧,现在让我写一些代码将它们应用在我们的app中。

    编译Vertex和Fragment Shaders

    在 - (instancetype) initWithFrame:(CGRect)frame 函数前加入如下函数:

    - (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(@"Unable read shader file:%@", shaderPath);
    } else {
    NSLog(@"Read shader file success! %@", shaderPath);
    }
    
    // 2
    GLuint shaderHandle = glCreateShader(shaderType);
    
    // 3
    const char* shaderStringUTF8 = [shaderString UTF8String];
    int shaderStringLength = (int)[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);
    }
    return shaderHandle;
    }
    

    OK,让我们看看这些代码如何工作的。

    1、拿到glsl文件的字符串内容。
    2、调用glCreateShader 去创建一个OpenGL对象来表示这个shader。当你调用这个函数的时候你需要传入一个参数,是vertex shader或者是fragment shader。
    3、调用glShaderSource告诉OpenGL这个shader的内容。在这里只是为了opengl的调用做了c类型的字符数组的转换。
    4、最后,调用glCompileShader完成运行时的编译。
    5、这个过程会失败,如果你的GLSL代码有问题会报错。如果错了,glGetShaderiv会获得更多有价值的信息。还缺少一个链接,让OpenGL使用shader程序。加入如下代码:

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

    让我们看下以上代码如何工作的:

    1、用之前封装的代码编译两个shader,返回shader句柄
    2、通过glCreateProgram,glAttachShader,glLinkProgram去链接两个shader到program
    3、通过glGetProgramiv,glGetProgramInfoLog获得必要的充分的错误信息
    4、通过glUseProgram告诉OpenGL当提供了vertex信息后真正的去使用它
    5、最后,通过glGetAttribLocation可以获得vertex shader中的变量指针,这些变量默认是无效的,

    通过代码将他们设置为有效。

    在initWithFrame调用render后,在调用 compileShaders函数。

    下面为简单的四边形创建顶点数据当你要通过OpenGL渲染任何一个几何图形的时候,记住它不能渲染四边形-只能渲染三角形。然而我们如果创建一个四边形可以通过创建两个三角形拼接而成。
    有一个比较好的事情是,在OpenGL ES2.0里你能以你喜欢的方式组织顶点数据。

    下面代码是一组c结构体和数组定义。

    // 一个顶点,位置:x,y,z和颜色:rgba
    
    typedef struct
    
    {
    
    float Position[3]; // 坐标:x,y,z
    
    float Color[4]; // 颜色:r,g,b,alpha
    
    } Vertex;
    
    // 一个四边形,包含了四个顶点的定义。包括每个顶点包含的坐标和颜色。
    
    const Vertex Vertices[] = {
    
    {{1,-1,0},{1,0,0,1}}, // v0, red
    
    {{1,1,0},{0,1,0,1}}, // v1, green
    
    {{-1,1,0},{0,0,1,1}}, // v2, blue
    
    {{-1,-1,0},{0,0,0,1}} // v3, black
    
    };
    
    // 代表两个三角形,组成了一个四边形
    
    const GLubyte Indices[] = {
    
    0,1,2,
    
    2,3,0
    
    };
    

    创建Vertex Buffer对象给OpenGL传递数据的最佳方式是调用Vertex Buffer Objects。
    基本上VBO是储存buffer和顶点数据的OpenGL对象。
    所以在initWithFrame中加入如下函数的调用。

    - (void) setupVBOs
    {
    GLuint vertexBuffer;
    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);
    GLuint indexBuffer;
    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
    }
    
    

    你可以发现它非常简单。它使用了和之前类似的方式创建Vertex Buffer,通过调用glGenBuffer创建一个VBO的句柄。通过glBindBuffer去告诉OpenGL:“嘿,当我说GL_ARRAY_BUFFER的时候,我的意思就是创建了VBO,我要给你数据啦”,然后通过调用glBufferData将VBO数据给OpenGL。
    给OpenGL配置了VBO后,需要修改render函数的实现,如下:

    - (void) render
    {
    glClearColor(0, 104.0/255.0, 55.0/255.0, 1.0);
    
    glClear(GL_COLOR_BUFFER_BIT);
    
    //1
    glViewport(0, 0, self.bounds.size.width, self.bounds.size.height);
    
    //2
    glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
    glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (GLvoid*)(sizeof(float)*3));
    
    //3
    glDrawElements(GL_TRIANGLES, sizeof(Indices)/sizeof(Indices[0]), GL_UNSIGNED_BYTE, 0);
    [_context presentRenderbuffer:GL_RENDERBUFFER];
    
    }
    

    1、调用glViewport 设置UIView渲染的部分。这里设置了整个屏幕,如果你想渲染一部分,可以修改此值。
    2、调用glVertexAttribPointer 设置两个vertex shader中的两个输入值。
    这是(glVertexAttribPointer)特别重要的函数,我们详细研究下:
    1)、第一个参数是之前获取的shader输入参数
    2)、第二个参数是制定多少个值传递个顶点。如果你查看Vertex结构,你会发现坐标position包含三个float,分别是x,y,z。然后颜色是四个float,rgba.
    3)、第三个参数指定每个参数的类型,坐标和颜色都是float类型。
    4)、第四个参数永远是false
    5)、第五个参数是设定参数的幅度,这是一种特殊的方式说:“这个数据的大小指的是每个顶点的数据”。所以我们提供给它真个顶点结构的大小。
    6)、第六个参数是如何找到对应数据的偏移量。坐标数据偏移是0,颜色数据是跳过3个float的长度作为偏移。

    3、调用glDrawElements让奇迹发生。这实际上最后调用了vertex shader 和 在屏幕的每一个像素上的fragment shader 。

    这也是一个很重要的函数:
    1)、第一个参数设定了绘制顶点的风格。会有诸多风格供选择,比如GL_LINE_STRIP、GL_TRIANGLE_FAN,但是GL_TRIANGLES一般是有用的(特别是连接Vertex Buffer Objects,VBO的时候),所以在这里设置了GL_TRIANGLES标志。
    2)、第二个参数是绘制三角形的个数,这个例子绘制一个四边形由两个三角形组成。这里我们用了C的方式计算个数。
    3)、第三个参数是每个私有索引的变量类型。
    4)、第四个参数参考文档,应该是一个指向indices的指针,但是我们在setupVBO函数中已经将数组通过GL_ELEMENT_ARRAY_BUFFER 传给opengl了。所以此处不需要传,设置为0.

    编译,会看到一个渐变的红、绿、蓝、黑的铺满屏幕的矩形。
    你可以能会奇怪为什么这个矩形适配屏幕这么合适。OpenGL有个“摄像头”在坐标原点(0,0,0),从Z轴俯视。
    显然,在实际的应用中你会希望更多的控制摄像头。所以我们看一下如何通过投影变换实现它。
    添加投影:
    让物体在二维场景中出现三维效果,我们需要在物体上增加投影变换。
    基本上,我们有一个近板和一个远板,所有我们要显示的物体都会在两个板子之间。近一些的物体接近近板,我们缩小它感觉离我们越近,反之放大一些看起来离我们更远。这也是模仿人类眼镜的工作。
    一起看看如何修改代码实现投影。需要修改Vertex.glsl脚本程序。代码如下:

    attribute vec4 Position; // 1
    
    attribute vec4 SourceColor; // 2
    
    varying vec4 DestinationColor; // 3
    
    uniform mat4 Projection; //  增加投射矩阵
    
    void main(void) { // 4
    
    DestinationColor = SourceColor; // 5
    
    gl_Position = Projection * Position; // 坐标重新计算
    
    }
    

    在此我们添加了一个新的输入变量 Projection。注意我们没有设置属性为attribute,而是定义为uniform类型。这意味着传入的是一个常亮而不是pre-vertex value.
    同样,Projection定义为mat4类型。mat4代表 4x4的矩阵。矩阵数学是一个非常大的课题,无法在这里展开
    讲清楚,但是现在你可以把它理解成为可以缩放、旋转、平移的顶点。我们传入一个矩阵,根据投影移动我们的顶点。

    下一步,我们设置最终的坐标为投影乘以坐标。在某物上做矩阵变化就像这样做。
    现在你需要想vertex shader传入投影矩阵参数。然而,复杂的矩阵只是已经在毕业多年忘在脑后该怎么办,

    别气馁,有聪明人已经做好了解决方案!

    聪明人比如Bill Hollings,他是Cocos3D的作者。他已经写了一个全功能的3D 图形库,已经很好的整合进了cocos2d。但无论如何,Cocos3D包含了很好的oc vector和matrix库,我们可以包含到我们的工程里。

    在compileShader函数中添加代码:

    _projectionUniform = glGetUniformLocation(programHandle, "Projection");
    

    获取 Projection矩阵变量的指针。

    在render函数 glViewport调用之前添加代码:

    CC3GLMatrix* projection = [CC3GLMatrix matrix];
    
    float h = 4.0 * self.frame.size.height / self.frame.size.width;
    
    [projection populateFromFrustumLeft:-2 andRight:2 andBottom:-h/2 andTop:h/2 andNear:4 andFar:10];
    
    glUniformMatrix4fv(_projectionUniform, 1, 0, projection.glMatrix);
    

    CC3GLMatrix是第三方封装好的类,可以设置左右前后的值。

    给vertex shader传值的方式是通过glUniformMatrix4fv,CC3GLMatrix类有一个友好的函数方法glMatrix,他能把矩阵转换成OpenGL能够识别的数组。
    最后一步是将我们的顶点扭动,让他们可以两个板之间(之前举例的近板和远板)。这个只需要在数组里修改z轴的值,这里由0改为-7.

    我们继续,下面添加平移和旋转。
    上面的代码中其实手动的去改-7本身就是不方便,甚至令人厌恶的做法。有没有更优雅的办法呢?

    有!

    哪些事情以及类似的事情都是矩阵变换,called Matrix Transform.他们使你移动顶点变得非常容易。

    到目前为止,我们的vertex shader修改了顶点的坐标通过矩阵变换,所以为什么不可以平移、缩放和旋转呢?
    我们将称之为“模型-视图”变换(model-view)。
    编译运行,可以看到和之前直接设置数组是同样的效果,但是没有动,是的,我们只是让它运行一次。
    我们通过CADisplayLink实现渲染。
    理想情况是opengl定时刷新和屏幕刷新保持一致,这样不会有卡顿等情况。

    幸运的是,苹果提供了一个方便的方式去实现,通过CADisplayLink!它非常方便使用,让我们开始吧。

    在View中加入如下代码:

    - (void)setupDisplayLink
    {
      CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(render:)];
      [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    }
    

    将initWithFrame中的render调用改为setupDisplayLink的调用。

    编译运行!酷!就是这样!

    通过CADisplayLink在系统的每一帧调用你的render,它将基于sin函数更新变换,所以这个四边形将左右来回移动。
    我不认为这是足够酷的,它如果增加旋转会更酷,然后它会看起来像真实的3D!
    在render函数的populateFromTranslation的调用后面加入代码:

    _currentRotation += displayLink.duration * 90;
    [modelView rotateBy:CC3VectorMake(_currentRotation, _currentRotation, 0)];
    

    矩阵变换会在x,y轴上做变换,z轴不动。

    编译运行,它看起来真是3D的!

    到目前为止,它看起来还不足够酷,我不叫讨厌四边形,是时候转成立方体了。

    非常简单,扩展顶点数组就可以了。配置如下:

    // 一个四边形,包含了四个顶点的定义。包括每个顶点包含的坐标和颜色。

    // 一个立方体的八个顶点
    
    const Vertex Vertices[] = {
    
    {{1,-1,0},{1,0,0,1}}, // v0, red
    
    {{1,1,0},{0,1,0,1}}, // v1, green
    
    {{-1,1,0},{0,0,1,1}}, // v2, blue
    
    {{-1,-1,0},{1,1,0,1}}, // v3, black
    
    {{1,-1,-1},{1,0,0,1}}, // v4, red
    
    {{1,1,-1},{0,1,0,1}}, // v5, green
    
    {{-1,1,-1},{0,0,1,1}}, // v6, blue
    
    {{-1,-1,-1},{1,1,0,1}} // v7, black
    
    };
    

    // 代表两个切面,组成了一个四边形

    const GLubyte Indices[] = {
    
    // front
    
    0,1,2,
    
    2,3,0,
    
    //Back
    
    4,6,5,
    
    4,7,6,
    
    //left
    
    2,7,3,
    
    7,6,2,
    
    //right
    
    0,4,1,
    
    4,1,5,
    
    //top
    
    6,2,1,
    
    1,6,5,
    
    //bottom
    
    0,3,7,
    
    0,7,4
    
    };
    

    编译运行,看起来确实是个立方体,但看起来别扭,有时看着是透明的!

    幸运的是我们可以通过景深测试来解决这个问题。通过depth testing,OpenGL可以z轴上的追踪,而且只是绘制前面没有任何物体遮挡的顶点。

    添加代码如下:

    - (void) setupDepthBuffer
    {
      glGenRenderbuffers(1, &_depthRenderBuffer);
    
      glBindRenderbuffer(GL_RENDERBUFFER, _depthRenderBuffer);
    
      glRenderbufferStorage(GL_RENDERBUFFER,
    
      GL_DEPTH_COMPONENT16,
    
      self.frame.size.width,
    
      self.frame.size.height);
    }
    

    setupDepthBuffer 函数创建了一个depth buffer,以相似的方式创建render buffer。然而,注意它使用了glRenderBufferStorage替换了context的成员方法renderBufferStorage。后者是只用于color的renderbuffer。

    然后我们调用 glFramebufferRenderbuffer 去关联刚创建的景深buffer和frame buffer。记住我怎么说frame buffer可以包含不同类型buffer的吗?这是我们第一次关联了新的buffer进入framebuffer。

    在render函数中,我们在每次屏幕刷新的时候都清除了景深buffer,开启景深测试。
    编译并运行,就会看到一个真实的立方体在做平移和旋转了。

    That's all

    请我喝杯茶

    相关文章

      网友评论

          本文标题:OpenGL ES 入门之iOS平台

          本文链接:https://www.haomeiwen.com/subject/niaugxtx.html