iOS开发工程师浅谈OpenGL,如何绘制基础图形,快速上手

纯个人学习笔记分享, 不喜勿喷,自行取关!
技术不缺乏缔造者,网络不缺乏键盘侠,但缺乏分享技术的源动力!

image

OpenGL 简介

OpenGL发展至今,已经20余年。作为一个成熟而久负盛名的跨平台的计算机图形应用程序接口规范,它已经被广泛应在游戏、影视、军事、航空航天、地理、医学、机械设计,以及各类科学数据可视化的领域。

并且随着网络和移动平台的飞速发展,异步突起的OpenGL ES 和 WebGL 标准也吸引了大批开发者的眼球。而这两者与OpenGL 本身同样有千丝万缕的联系。

OpenGL 几乎支持所有现有的主流操作系统平台,包括Windows、Mac OS X以及各种UNIX平台。它同时也可以用于几乎所有主流的编程语言环境中,例如C/C++、Java、C#、Visual Basic、Python等。因此,OpenGL应当是全球最为广泛学习和使用的图形开发API接口。

Open GL 概述

什么是OpenGL

OpenGL 是一种应用程序编程接口(Application Programming Interface,API),它是一种可以对图形硬件设备特性进行访问的软件库。包含了500个不同的命令,可以用于设置所需的对象、图像和操作,以便开发交互式的3维计算机图形应用程序。

OpenGL 被设计为一个现代化的,硬件无关的接口。因此不需要考虑计算机操作系统和窗口系统的前提下,在多种不同的图形硬件系统上,或者完全通过软件的方式实现OpenGL 接口。

OpenGL 自身是不包含任何执行的窗扣任何或者处理用户输入输出的函数。事实上我们需要通过应用程序所运行的窗口系统提供的接口来执行这类操作。

OpenGL也没有提供任何用户表达3维物体模型,或者读取图像文件的操作。我们需要通过一系列几何图元(包括点、线、三角形以及patch)来创建三维空间的物体。

OpenGL 程序

  • 渲染:表示计算机从模型创建最终图像的过程。OpenGL 只是一种基于光栅化的系统。
  • 模型(场景对象):通过几何图元(点、线、三角形)来构建的。
  • 着色器,它是图形硬件设计所执行的一类特殊的函数。可以理解为图像处理单元(GPU)编译的一种小型程序。
  • 四种不同的着色阶段(shander stage),其中最常用的包括顶点着色器(vertex shader)以及片元着色器,前者用于处理顶点数据,后者用于处理光栅化后的片元数据。所有OpenGL程序都需要用到这两类着色器
  • 帧缓存(framebuffer),像素(pixel),是显示器上最小的可见单元。计算机系统将所有的像素保存到帧缓存当中,后者是有图形硬件设备管理的一块独立内存区域,可以直接映射到最终的显示设备上

什么叫 光栅化?
将输入图元的数学描述转为屏幕位置对应的像素片元,称为关栅化

OpenGL 渲染图像的OpenGL 程序需要执行的操作:

  • 从OpenGL的几何图元中设置数据,用于构建形状。
  • 使用不同的着色器(shader)对输入的图元数据执行计算操作,判断它们的位置、颜色,以及其他渲染属性。
  • 将输入图元的数学描述转化为与屏幕位置对应的像素片元(fragment)。这一步也称为光栅化(rasterization)。
  • 最后,针对光栅化过程产生的每个片元,执行片元着色器(fragment shader),从而决定这个片元的最终颜色和位置。
  • 如果有必要,还需要对每个片元执行一些额外的操作,例如判断片元对应的对象是否可见,或者将片元的颜色与当前屏幕位置的颜色进行融合。

案例1 绘制一个正方形

#include <iostream>
#include <GLUT/GLUT.h>

void draw() {

    //设置清屏色
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    //设置颜色,红色
    glColor3f(1.0f, 0.0f, 0.0f);
    //设置绘图时的坐标系统
    glOrtho(0.0f, 1.0f, 0.0f, 1.0f, -1.0f, 1.0f);
    //开始渲染
    glBegin(GL_POLYGON);
    //设置多边形的4个顶点
    glVertex3f(0.25f, 0.25f, 0.0f);
    glVertex3f(0.75f, 0.25f, 0.0f);
    glVertex3f(0.75f, 0.75f, 0.0f);
    glVertex3f(0.25f, 0.75f, 0.0f);
    //结束渲染
    glEnd();
    //强制刷新缓冲区,保证绘制命令被执行
    glFlush();

}

int main(int argc, const char* argv[]) {
    //初始化GLUT库
    glutInit(&argc, (char**)argv);
    //创建一个窗口并制定窗口名
    glutCreateWindow("HelloWorld");
    //注册一个绘图函数,操作系统在必要时刻就会对窗体进行重新绘制操作
    glutDisplayFunc(draw);
    //进入GLUT事件处理循环,让所有的与“事件”有关的函数调用无限循环(永生循环)
    glutMainLoop();
    return 0;
}

OpenGL语法

使用GLUT这个作为函数名的都是使用了GLUT库的

OpenGL 渲染管线

什么是渲染管线?
它是一系列数据处理过程,并且将应用程序的数据转换到最终的渲染图像。

OpenGl 首先接收用户提供的几何数据(顶点和几何图元),并且将它输入到一系列着色器阶段中进行处理,包括:顶点着色、细分着色、以及最后的几何着色,然后它将进入光删化单元。光栅化单元负责对所有剪切区域内的图元生成片元数据,然后对每个生成的片元都执行一个片元着色器。

事实上,只有顶点着色器 和 片元着色器是必须的。细分和几何着色器是可选的步骤。

案例2 简单绘制一个三角形

2.1要导入什么框架

  • #include<GLTools.h> GLTool.h头文件包含了大部分GLTool中类似C语言的独立函数
  • #include<GLShaderManager.h> 移入了GLTool 着色器管理器(shader Mananger)类。没有着色器,我们就不能在OpenGL(核心框架)进行着色。着色器管理器不仅允许我们创建并管理着色器,还提供一组“存储着色器”,他们能够进行一些初步䄦基本的渲染操作。
  • 在Mac 系统下,#include<glut/glut.h>
  • 在Windows 和 Linux上,我们使用freeglut的静态库版本并且需要添加一个宏。
    #define FREEGLUT_STATIC
    #include<GL/glut.h>

2.2 启动GLUT

1.程序的总是“main”函数开始处理
GLTools函数glSetWorkingDrectory用来设置当前工作目录。实际上在Windows中是不必要的,因为工作目录默认就是与程序可执行执行程序相同的目录。但是在Mac OS X中,这个程序将当前工作文件夹改为应用程序捆绑包中的/Resource文件夹。GLUT的优先设定自动进行了这个中设置,但是这样中方法更加安全。

2.创建窗口并设置显示模式
glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
GLUT_DOUBLE:双缓存窗口,是指绘图命令实际上是离屏缓存区执行的,然后迅速转换成窗口视图,这种方式,经常用来生成动画效果;
GLUT_DEPTH:标志将一个深度缓存区分配为显示的一部分,因此我们能够执行深度测试;
GLUT_STENCIL:确保我们也会有一个可用的模板缓存区。
深度、模板测试后面会细致讲到。

3.初始化GLEW库
重新调用GLEW库初始化OpenGL 驱动程序中所有丢失的入口点,以确保OpenGL API对开发者完全可用。
调用glewInit()函数一次就能完成这一步。在试图做任何渲染之前,还要检查确定驱动程序的初始化过程中没有出现任何问题。

4.SetupRC()
实际上这个函数对GLUT 没有什么影响,但是在实际开始渲染之前,我们这里进行任何OpenGL 初始化都非常方便。这里的RC代表渲染环境,这是一个运行中的OpenGL状态机的句柄。在任何OpenGL 函数起作用之前必须创建一个渲染环境。而GLUT在我们第一次创建窗口时就完成了这项工作。

5.初始化设置
void glClearColor(GLclampf red,GLclampf green,GLclampf blue,GLclampf alpha);

在windows 颜色成分取值范围:0-255之间
在iOS、OS 颜色成分取值范围:0-1之间浮点值
![常见颜色值表](/Users/liuyi/Documents/潭州教育/VIP Open GL/第一天/01 备课/01–OpenGL初览下/课件/颜色值表.png
)

#include "GLShaderManager.h"

#include "GLTools.h"

#include <glut/glut.h>

//简单的批次容器,是GLTools的一个简单的容器类。
GLBatch triangleBatch;

GLShaderManager shaderManager;

//窗口大小改变时接受新的宽度和高度,其中0,0代表窗口中视口的左下角坐标,w,h代表像素

void ChangeSize(int w,int h)

{

    glViewport(0,0, w, h);

}

//为程序作一次性的设置

void SetupRC()

{

    //设置背影颜色

    glClearColor(0.0f,0.0f,1.0f,1.0f);

    //初始化着色管理器

    shaderManager.InitializeStockShaders();

    //设置三角形,其中数组vVert包含所有3个顶点的x,y,笛卡尔坐标对。

      GLfloat vVerts[] = {

        -0.5f,0.0f,0.0f,

        0.5f,0.0f,0.0f,

        0.0f,0.5f,0.0f,

    };

    //批次处理

    triangleBatch.Begin(GL_TRIANGLES,3);

    triangleBatch.CopyVertexData3f(vVerts);

    triangleBatch.End();

}

//开始渲染

void RenderScene(void)

{

    //清除一个或一组特定的缓冲区
    //缓冲区是一块存在图像信息的储存空间,红色、绿色、蓝色和alpha分量通常一起分量通常一起作为颜色缓存区或像素缓存区引用。
    //OpenGL 中不止一种缓冲区(颜色缓存区、深度缓存区和模板缓存区)。

    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT|GL_STENCIL_BUFFER_BIT);

    //设置一组浮点数来表示红色

    GLfloat vRed[] = {1.0f,0.0f,0.0f,1.0f};

    //传递到存储着色器,即GLT_SHADER_IDENTITY着色器,这个着色器只是使用指定颜色以默认笛卡尔坐标第在屏幕上渲染几何图形
    //没有着色器,在OpenGL 核心框架中就无法进行任何渲染。在后面的课程中我们讲到不用固定渲染管线,当然在前期会先学习如果使用存储着色器。

    shaderManager.UseStockShader(GLT_SHADER_IDENTITY,vRed);

    //提交着色器

    triangleBatch.Draw();

    //将在后台缓冲区进行渲染,然后在结束时交换到前台

    glutSwapBuffers();

}

int main(int argc,char* argv[])

{

    //设置当前工作目录,针对MAC OS X
    /*
    `GLTools`函数`glSetWorkingDrectory`用来设置当前工作目录。实际上在Windows中是不必要的,因为工作目录默认就是与程序可执行执行程序相同的目录。但是在Mac OS X中,这个程序将当前工作文件夹改为应用程序捆绑包中的`/Resource`文件夹。`GLUT`的优先设定自动进行了这个中设置,但是这样中方法更加安全。
    */
    gltSetWorkingDirectory(argv[0]);

    //初始化GLUT库

    glutInit(&argc, argv);

    /*初始化双缓冲窗口,其中标志GLUT_DOUBLE、GLUT_RGBA、GLUT_DEPTH、GLUT_STENCIL分别指

     双缓冲窗口、RGBA颜色模式、深度测试、模板缓冲区
  2.创建窗口并设置显示模式
`glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);`
`GLUT_DOUBLE`:双缓存窗口,是指绘图命令实际上是离屏缓存区执行的,然后迅速转换成窗口视图,这种方式,经常用来生成动画效果;
`GLUT_DEPTH`:标志将一个深度缓存区分配为显示的一部分,因此我们能够执行深度测试;
`GLUT_STENCIL`:确保我们也会有一个可用的模板缓存区。
深度、模板测试后面会细致讲到
     */

    glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);

    //GLUT窗口大小,标题窗口

    glutInitWindowSize(800,600);

    glutCreateWindow("Triangle");

    //注册回调函数

    glutReshapeFunc(ChangeSize);

    glutDisplayFunc(RenderScene);

    //驱动程序的初始化中没有出现任何问题。

    GLenum err = glewInit();

    if(GLEW_OK != err) {

        fprintf(stderr,"glew error:%s\n",glewGetErrorString(err));

        return 1;

    }

    //调用SetupRC

    SetupRC();

    glutMainLoop();

    return 0;

}

OpenGL 常见流程

image

  • 顶点着色器 和 片元着色器是必需的。细分和几何着色器是可选的捕捉。
  • OpenGL 需要将所有的数据都保存到缓存对象中(buffer object)

将数据输出到OpenGL

当将缓存数据初始化完毕后,我们可以通过调用OpenGL 的一个绘制命令来请求渲染几何图元。使用的是glDrawArrays()就是一个常用的绘制命令

OpenGL 的绘制通常都是讲顶点数据传输到OpenGL 服务端。我们可以将一个顶点视为一个需要统一处理的数据包。这个包装的数据可以是我们需要的任何数据(也就课),通常其中几乎始终会包含位置数据。其他数据可能用来觉得一个像素的最终颜色。

小编给大家推荐一个iOS技术交流群:551346706!群内提供数据结构与算法、底层进阶、swift、逆向、底层面试题整合文档等免费资料!

简单介绍关键概念

  • 顶点着色:对于绘制命令传输的每个顶点,OpenGL都会调用一个顶点着色器来处理顶点相关的数据。只是将数据复制并传递到下一个着色阶段,叫做传递着色器(pass-through shader)。通常来说,一个复杂的应用程序可能包含许多顶点着色器,但在同一时刻只能有一个顶点着色器起作用
  • 细分着色:顶点着色器处理每个顶点的关联数据之后,如果同时激活了细分着色器,那么它将进一步处理这些数据。(第9章介绍)
    细分着色器阶段会用到两个着色器来分别管理Patch数据并产生最终的形状。
  • 几何着色:允许在光栅化之前对每一个几何图元做更进一步的处理,例如创建新的图元。这个着色阶段是可选的。我们在后面的课程详解
  • 图元装配:图元装配将顶点及相关的集合图元之间组织起来,准备下一步剪切和光栅化操作
  • 剪切:顶点可能落在视口(viewport)之外,此时与顶点相关的图元会做出改动,以保证相关的像素不会在视口外绘制。剪切(clipping)由OpenGL自动完成。
  • 光栅化:将更新后的图元(primitive)数据传递到光栅化单元,身材对应的片元(fragment).我们将一个片元是为一个“候选的像素”。也就是可以放置在帧缓存(framebuffer)中的像素,但是它也可能被最终剔除,不再更新对应的像素位置。之后两个阶段将会执行片元的处理。
  • 片元着色:最后一个可以通过编程控制屏幕上显示颜色的阶段。在Fragment Shader阶段中,我们使用着色器计算片元的最终颜色和它的深度值。

顶点着色和片元着色器的之间的区别?
顶点着色器(包括细分和几何着色)决定了一个图元应该位于屏幕的什么位置,而片元着色使用这些信息来决定某一个片元的颜色应该是什么?

  • 逐步元的操作:这个阶段里会使用深度测试(depth test )和模板测试(stencil test)的方式来决定一个片元是否可见的。

main函数里的常用函数

glutInit() 负责初始化GLUT库。它会处理向程序输入的命令行参数,并且移除其中与控制GLUT如何操作相关的部分。它必须是应用程序第一个GLUT函数,负责设置其他GLUT例程必需的数据结构。

glutInitDisplayMode() 设置了程序所使用的窗口类型。窗口设置更多的OpenGL 特性,例如RAGA颜色空间,使用深度缓存或动画效果。

glutInitWindowsSize() 设置所需的窗口大小,如果不想在这个设置一个固定值,也可以先查询显示设备的尺寸,然后根据计算机的屏幕动态设置窗口的大小。

glutCreateWindow(),它的功能和它的名字一样,如果当前的系统环境可以满足glutInitDisplayMode()的显示模式要求,这里就会创建一个窗口(此时会调用计算机窗口系统的接口)。只有GLUT创建了一个窗口之后(其中包含创建创建OpenGL环境的过程),我们才可以使用OpenGL相关的函数

glewInit()函数,属于另一个辅助库GLEW(OpenGL Extention Wrangler)。GLEW可以简化获取函数地址的过程,并且包含了可以跨平台使用的其他一些OpenGL编程方法。

glutDisplayFunc(),它设置了一个显示回调(diplay callback),即GLUT在每次更新窗口内容的时候回自动调用该例程

glutMainLoop(),这是一个无限执行的循环,它会负责一直处理窗口和操作系统的用户输入等操作。(注意:不会执行在glutMainLoop()之后的所有命令。)

初始化顶点数组对象

void glGenVertexArray(GLsizei n ,GLUINT * arrays)
分配顶点Arr对象
返回n个未使用的对象名到数组arrays中,用作顶点数组对象。返回的名字可以用来分配的缓存对象,并且它们已经使用未初始化的顶点数组集合的默认状态进行了数值的初始化。

void glBindVertexArray(GLuint array);
绑定激活对象数组
glBindVertexArray(),如果输入的变量array非0,并且是glGenVertexArrays()所返回的,那么它将创建一个新的顶点数组对象并且与其名称关联起来。如果绑定到的是一个已经创建的顶点数组对象中,那么会激活这个顶点数组对象,并且直接影响对象找那个所保存的顶点数组对象,并且将渲染状态重设为顶多数组的默认状态。
如果array不是glGenVertexArrays(),所返回的数值,或者它已经被glDeleteVertexArray()函数释放,那么这里将产生一个GL_INVALID_OPERATION错误。

在2种情况下,我们需要绑定一个对象:
    1.创建一个对象并初始化它所对应的数据时;
    2.每次准备使用这个对象,而不是当前绑定的对象时。

当我们完成对顶点数组对象的操作之后,是可以调用glDeleteVertexArrays()将它释放的。

void glDeleteVertexArrays(GLsizei n,GLuint *arrays);
删除n个在arrays中定义的顶点数组对象,这样所有的名称可以再次用作顶点数据。如果绑定顶点数组已经被删除,那么当前绑定的顶点数组对象被重设为0(类似执行了glBindBuffer()函数,并且输入参数为0)。默认的顶点数组会变成当前对象。在arrays当中未使用的名称都会被释放,但是当前顶点数组的状态不会发生任何变化

为了保证程序的完整性,可以调用gllsVertexArray()检查某个名称释放已经被保留为一个顶点数组对象了。
GLboolean gllsVertexArray(GLuint array);
如果array是一个已经用GLgenVertexArrays()创建且没有被删除的顶点数组对象的名称,那么返回GL_TRUE,如果array为0或者不是任何顶点数组对象的名称,那么返回GL_FALSE;

分配顶点缓存对象

顶点数组对象负责保存一系列的数据,这些数据保存到缓冲对象中,并且由当前绑定的顶点数组对象管理。缓存对象就是OpenGL 服务端分配和管理的一个块内存区域,并且几乎所有传入的OpenGL的数据都存储在缓存对象当中。
顶点缓冲对象的初始化过程与顶点数组对象的创建过程类似,不过需要有向缓存添加数据的一个过程。

1.创建顶点缓存对象的名称
void glGenBuffers(GLsizei n,GLuint *buffers);

返回n个当前未使用的缓存对象名,并保存到buffers数组中。返回到buffers中的名称不一定是连续的整型数据。
这里返回的名称只用于分配其他缓存对象,它们在绑定之后只会记录一个可用状态。
0是一个保留的缓存对象名称,glGenBuffers()永远都不会返回这个值的缓存对象。

2.绑定缓存对象
由于OpenGL 中有很多不同类型的缓存对象,因此绑定一个缓存时,需要指定对应的类型。

1.指定当前激活/绑定的对象 
void glBindBuffer(GLenum target,GLunit buffer);

参数target的类型:GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER、GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER、GL_TRANSFORM_FEEDBACK_BUFFER、GL_UNIFORM_BUFFER。
顶点数据缓存使用GL_ARRAY_BUFFER

参数buffer:绑定的缓存对象名称

glBindBuffer完成3项工作:1.如果是第一次绑定buffer,且它是一个非零无符号整型,那么将创建一个与该名称相对应的新缓存对象。2.如果绑定到是一个已经创建的缓存对象,那么它将成为当前被激活的缓存对象。3.如果绑定的buffer值为0,那么OpenGL将不在对当前target应用任何缓存对象。

2.释放缓存
void glDeleteBuffers(GLSizei n,const GLuint *buffers);

删除n个保存在buffer数组中的缓存对象。被释放的缓存对象可以重用。
如果删除的缓存已经绑定,那么该对象所以绑定将会重置为默认缓存对象,即相当于用0作为参数执行glBindBuffer(),如果试图删除不存在的缓存对象,或者缓存对象为0,那么将忽略该操作(不会产生错误)

3.判断一个整数值是否是一个缓存对象的名称。
GLboolean gllsBuffer(GLuint buffer);
如果buffer是一个已经分配并且没有释放的缓存对象名称,则返回GL_TRUE。如果buffer为0或者不是一个缓存对象的名称,则返回GL_FALSE。

将数据载入缓存对象

初始化顶点缓存对象之后,我们需要把顶点数据从对象传输到缓存对象中。这一步步是通过glBufferData()来实现的。它主要有2个任务:分配顶点数据所需的存储空间,然后将数据从应用程序的数组拷贝到OpenGL 服务端的内存中

glBufferData(GLenum target,GLsizeiptr size,const GLVoid *data,Glenum usage);

当OpenGL 服务端内存中分配size个存储单元(通常都是byte),用于存储数据或者索引。如果当前绑定的对象已经存在了关联数据,那么首先会删除这些数据。

参数target:
顶点属性数据,GL_ARRAY_BUFFER;
索引数据,索引数据,GL_ELEMENT_ARRAY_BUFFER;
从OpenGL 中获取的像素数据,GL_PIXEL_PACK_BUFFER;
OpenGL 的像素数据GL_PIXEL_UNPACK_BUFFER;
对于缓存直接的复制数据,GL_COPY_READ_BUFFER、GL_COPY_WRITE_BUFFER;
对于通过transform feedback 着色器获得结果,GL_TRANSFORM_FEEDBACK_BUFFER、
一致变量,GL_UNIFORM_BUFFER。
对于纹理缓存中存储的纹理数据,GL_TEXTURE_BUFFER

参数size:存储数据的总数量,data存储元素的总数*单位元素存储空间
参数data: 客户端内存的指针,以便初始化缓存对象,要么是NULL,如果传入的指针合法,那么将会有size个大小的数据从客户端拷贝到服务端。如果传入的是NULL,那么将保留size大小的未初始化的数据,以备后用。

如果所需的size大小超过了服务端能够分配的额度,那么glBufferData()将产生一个GL_OUT_OF_MEMORY错误。如果usage设置的不是可用的模式值,那么会产生GL_INVALID_VALUE错误。

一下子理解这么多内容,可能会有些困难。但是这些函数在后面的课程学习中会重复多次出现。

初始化顶点与片元着色器

对于每一个OpenGL 程序,当使用的OpenGL 版本高于或者等于3.1时,都需要指定至少2个着色器:顶点着色器 和 片元着色器。

对于OpenGL程序员而言,着色器使用OpenGL着色语言(OpenGL shading Language,GLSL)编写的小型函数。GLSL是构成所有OpenGL着色器的语言。它与C++语言非常类似。用GLSL中的所有特性并不能用于OpenGL每一个着色阶段。当我们可以以字符串的形式传输GLSL着色器到OpenGL。

为了让学员更容易使用着色器开发,我们选择将着色器字符串的内存保存到文件,并且使用**LoadShaders()**读取文件和创建OpenGL着色程序。

小编给大家推荐一个iOS技术交流群:551346706!群内提供数据结构与算法、底层进阶、swift、逆向、底层面试题整合文档等免费资料!

猜你喜欢

转载自blog.csdn.net/weixin_42362587/article/details/85255706