1 前言
一直想沿着图像处理这条线建立一套完整的理论知识体系,同时积累实际应用经验。因此有了从使用AVFoundation捕捉图像,OpenCV识别图像,OpenGL渲染场景的学习计划,另外机器学习和深度学习在多媒体编码及图像处理中的应用也在计划之中。
在简书上写博客的目的有两个,其一是为了记录自己的学习历程,并把这作为自己的一个学习笔记,在需要的时候方便查阅。另外就是希望通过这种方式结识更多有着相同爱好的开发者,能够一起交流和讨论这个方向的知识。
该系列文章为OpenGL蓝宝书OpenGL SuperBible第6版英文版读书笔记,运行环境macOS 10.12.6,Xcode 9.0.1。在Mac上不能使用该书的最新版第7版,因为第7版的使用Open GL4.5,然而即使是最新的Macbook pro也不支持该版本。其实苹果已经使用Metal替代了OpenGL,并且将OpenGL相关的接口都标注为废弃,但是这并不表示学习OpenGL的知识是无用的。
关于资料,本系列文章全部都配有MacOS或者iOS环境中的Demo,并且每个例子都准备有Start和Final两个工程,后者是完整的示例程序,运行后可以得到和文章配图相同的效果。而Start结尾的工程是为了能够让看到篇文章,并相联系的小伙伴能够更容易的实现自己的程序,在该项目中移除了关键的代码,这样在练习时就可以不必花太多时间在无用的代码上,只需要补全关键的代码即可。
在Mac上,GPU定义了物理支持的OpenGL版本,Mac OS框架定义了支持的GL API版本。获取前者的信息除了之前提到的在苹果官网中查看,还可以使用AppStore中一个OpenGL Extension Viewer查看。后者信息在NSOpenGLContext中讲解。
另外,查询你的Mac能支持的OpenGL版本移步至Mac设备支持的OpenGL列表。查询你的GPU能够支持的特性请移步至苹果设备GPU型号及其在不同的环境下支持的OpenGL特性。我的Mac支持OpenGL4.1 Profile。因此选用OpenGL SuperBible 6th,但这个版本书使用的OpenGL版本是OpenGL4.3 Profile,因此书中部分功能无法使用。具体各版本OpenGL支持的功能特性移步至OpenGL历史版本。
2 概览
本节的主要知识点如下。对于文中关于OpenGL的详细的知识点有所疑惑请参考后面的章节,本篇文章的目的不是详细的讲解3D图形是如何绘制并且渲染的,而是让读者对OpenGL有一个大概的了解。
- 对OpenGL的起源,演变,功能进行介绍。
- 介绍了OpenGL的竞争者。
- 介绍了如何在iOS平台和MacOS平台上搭建OpenGL程序。
3 OpenGL简介
OpenGL是一个跨平台的图形编程接口,他可以在Linux、Windows、Mac OS、iOS、Android等各种平台上使用。
OpenGL的目标是在程序和底层图形处理子系统之间提供一个抽象的层,其中底层图形处理子系统通常由一个或者多个带有专用存储的高性能处理器组成。OpenGL需要平衡它的抽象程度,使它能够屏蔽不同硬件和系统带来的编程复杂性,同时兼顾能够充分使用硬件特性。游戏引擎的抽象程度通常很高,视频游戏机(video game consoles)的抽象程度通常非常低。
现代GPU展现出的强大计算能力使它不仅仅局限于图形处理,它还可以在物理模拟、人工智能甚至音频处理中得到充分利用。现代CPU由大量被称为着色器核心(shader core)的小型可编程处理器组成。每个核心都运行着迷你程序(shader),执行简单有限和单一的计算指令。
3.1 起源和发展
OpenGL起源于Silicon Graphics公司的IRIS图像实验室,最早是为其高端显卡服务的专有API,1992年时将其开源发布OpenGL 1.0版本。当年,OpenGL架构审核委员会(ARB)在其组织下成立,最初成员包括IBM, Intel, and Microsoft等公司。ARB负责设计管理和发布OpenGL描述和规范。现在该委员会是Khronos Group的一部分,OpenGL目前最新的版本是4.6。
2008年ARB将OpenGL分为两个部分,core profile和compatibility profile。core profile只保留现代GPU支持的较新部分接口。后者支持至OpenGL 1.0的接口。在开始新项目是应该使用前着,维护老项目时使用后者。值得注意的是,在部分平台上,更新的特性只支持在core profile中使用。另外使用前者编写的程序其运行速度也要高于后者。
3.2 竞争者
3D图形处理常用的两个编程语言是OpenGL ARB主导的OpenGL以及Microsoft主导的Direct3D。OpenGL更早发布,但是随后由于Direct3D支持可编程图形硬件特性,并且能及时支持新的GPU特性迅速抢占了OpenGL地位。在DirectX 10后其跟随最新的Windows系统绑定发布新版本,并且更新速度变慢,导致不能及时支持GPU生产商发布的新型GPU特性。OpenGL的ARB委员会加速其研发速度,现在它同样支持可编程图形硬件特性,并且较于Direct3D它及时支持新型GPU特性,ARB正在让它重新占领市场的领导地位。
另外如前文说到,苹果于2014年发布了自己的图像渲染框架Metal,并且宣称和OpenGL相比,其具有更好的渲染性能。在2016年,负责维护OpenGL的ARB委员会发布了他们的新一代图像渲染框架VulKan,这被认为是OpenGL的接任者,该框架已经得到了来自工业局各个巨头的积极响应。ARB称VulKan具有比OpenGL更高的性能,这里也有着重新夺回游戏市场地位的意图。下图介绍了主要的图像渲染引擎开发历史。
3.3 图像管道
OpenGL的工作方式类似工厂中的流水线,OpenGL接受来自程序的命令,并将这些命令发送到底层的图形硬件。在图形硬件中,大量的指令排队并发运行,队列中某个指令前阶段可能后另一个指令的后阶段同时执行。进一步说,计算机图形通常由很多重复的任务块(如计算一个像素应该是什么颜色)组成,这些任务之间相互独立。OpenGL将程序发送给它的工作分解为很基础并且并行的任务块。现代的图形处理器通过极高的并发运算达到非常高的性能。
这种流水线的概念在OpenGL中被称为图像管道,图形处理可以分为几个阶段,每个阶段由一个着色器或者一个固定函数组成。下图描述了图像管道中的所有阶段。
图中蓝色的矩形表示可编程的阶段,我们可以通过编写OpenGL指定类型的着色器来自定义这些阶段的图像处理逻辑。绿色的矩形代表固定函数,即我们不能控制其内部实现,但是实际上一些固定函数仍然是用着色器语言实现的,只是它由GPU供应商提供预制的代码。
3.4 图元、管线和像素
当收到来自程序的命令时,数据从管道的一端流入,在流动的过程中,着色器程序(Shader)和固定函数会从缓存(Buffer)和纹理(Texture)中抓取更多的数据。某些阶段甚至存储数据到这些缓存和纹理内,最后在管道另一端返回处理完成的数据。
OpenGL中基本的渲染单元为图元(Primitive),OpenGL支持多种图元,他们由点、线和三角这三个基本图元组成。程序将复杂表面分解为大量三角形图元然后将其交付给GPU,GPU使用其内置的光栅器(Rasterizer)将其转化为一些列的像素点用于随后的渲染操作。
点、线和三角分别由一个、两个或多个顶点组成,顶点主要位于三维空间坐标系中。图形管道通常分为两部分,第一部分称为前端(Front End),它负责处理顶点和图元,最终将它们转化为点、线和三角,然后将其交付给光栅器,这一过程称为初始装配(Primitive Assembly)。经过光栅化器后,由向量构成的几何图形被转化为大量相互独立的像素点。从这里开始进入后端(Back End)的处理过程,其中包括深度(Depth)和模板(Stencil)测试、片段着色、颜色混合最终得到输出图像。
4 MacOS环境搭建
在Metal未发布之前,OpenGL在Apple眼中的地位正如Direct3D在Microsoft中一样,Apple为OpenGL的研发提供了很大的支持。但是由于Apple关注用户的体验,希望当前的应用能在Apple系列的所有设备中正常运行,因此Apple官方仅仅采用成熟的OpengGL版本,一些新的特性在Apple中以扩展的形式存在。
在Mac OS中,系统处理提供原生的OpenGL框架,还提供CGL、GLKit框架,以及在AppKit框架提供NSOpenGL相关的控件辅助我们开发程序。它们的功能描述见下表。除此之外我们还能使用一些第三方的框架,如GLUT等技术使用OpenGL。
名字 | 描述 |
---|---|
CGL | 最底层的OpenGL接口 |
NSOpenGL | 基于OpenGL部分接口进行面向对象的封装 |
GLKit | iOS中的一个OpenGL辅助开发工具库,其中部分功能在MacOS上也能使用 |
4.1 Core OpenGL
Core OpenGL是最底层的框架,它可以很好的和NSOpenGL中、GLUT或者其他更高级的第三方框架协同工作。另外CGL可以不使用NSOpenGL中视图部分直接创建全屏幕的context,但这在现代的OS X上已经不被鼓励使用。大多数Core OpenGL的方法都需要使用CGL context作为参数,获取当前的CGL context的方法是CGLGetCurrentContext(void)。
4.2 NSOpenGL
NSOpenGL主要包含视图(NSOpenGLView)和配置(NSOpenGLContext)两部分内容,其中后者主要用于配置OpenGL的版本信息等OpenGL方面的配置信息,以及一些视图中使用的渲染颜色空间等信息。
NSOpenGLView主要提供以下4个方法,各个方法的调用时间和必要的配置信息如下。示例使用Storyboard的方式创建NSOpenGLView,尽管UI界面上可以配置部分OpenGL Context的属性,但是由于某些配置仍无法完成。通常都需要使用代码配置,并且需要注意的是,代码中重新设置了上下文,这会使UI界面上的配置全部失效。通常需要在OpenGL Context中配置Color为32bit RGBA、Depth为24bit、Stencil为8bit、Renderer为Accelerated Renderer。更多配置和其具体意义参考头文件和Apple官网。
// Initialize OpenGL view and configure OpenGL Context. Several Contexts can be exist
// on application, but there are only one can be current context for a given thread.
// Moreover, OpenGL Context can share resouces so we can upload a texture or anything
// that needed for redering by a background context and then pass these resources to next context.
- (instancetype)initWithCoder:(NSCoder *)decoder {
// NSOpenGLProfileVersionLegacy is default
// NSOpenGLPFAColorSize must be always match the screen's color depth
// 对于BOOL的key,只需要在数组中包含这个key值就表示使用这个配置,对于有具体数值的key值,
// 其后需要紧跟一个数值,数组以0作为结束标志。
NSOpenGLPixelFormatAttribute pixelFormatAttributes[] = {
NSOpenGLPFAColorSize, 32,
NSOpenGLPFADepthSize, 24,
NSOpenGLPFAStencilSize, 8,
NSOpenGLPFAAccelerated,
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion4_1Core,
0
};
NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:pixelFormatAttributes];
NSOpenGLContext *openGLContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil];
if (self = [super initWithCoder:decoder]) {
[self setOpenGLContext:openGLContext];
[openGLContext makeCurrentContext];
}
return self;
}
// This method is called once time only after initialization
- (void)prepareOpenGL {
glClearColor(1.0f, 0.1f, 0.1f, 1.0f);
NSLog(@"Version: %s", glGetString(GL_VERSION));
NSLog(@"Renderer: %s", glGetString(GL_RENDERER));
NSLog(@"Vendor: %s", glGetString(GL_VENDOR));
NSLog(@"GLSL Version: %s", glGetString(GL_SHADING_LANGUAGE_VERSION));
}
// This method will call after "prepare" method when OpenGL view is created or each resization of the OpenGL view.
- (void)reshape {
NSRect bounds = [self bounds];
glViewport(0, 0, NSWidth(bounds), NSHeight(bounds));
}
// 这个方法执行具体的渲染逻辑
- (void)drawRect:(NSRect)dirtyRect {
glClear(GL_COLOR_BUFFER_BIT);
// “glFlush" is istead of some sort of buffer swap call. On Mac OS X, desktop compositing
// engine can be saw as front buffer, all OpenGL windows are really single buffered. Given
// OpenGL windows are always rendering to an off-screen buffer, when "glFlush" method is
// called, system will integrate the openGL rendering to the rest of teh desktop.
glFlush();
}
4.3 GLKit
GLKit是为了更好从OpenGL ES 1.x 的固定管道编程过渡到使用可编程管道的OpenGL ES 2.0设计的。最初只存在于iOS 5.0平台上,在OS X 10.8时引入Mac OS平台中。GLKit主要包含4个内容,纹理加载、数学库、特效和视图控制器。其中视图控制器部分只能在iOS平台中使用。特效部分主要是对部分固定函数管道(fixed-function pipeline)的进一步包装。但这对于我们深入理解OpenGL的实现原理有反面影响,因此不会介绍这个点。
OpenGL处理3D模型是靠向量和矩阵作为数学基础的,在OpenGL库中并不支持这些复杂的运算。于与其他平台需要额外的第三方数学库相比。Apple在GLKit中包含了数学库,数学库的主要目的是进行复杂的向量和矩阵运算,GLKMatrixLookAt甚至可以创建一个基于镜头的矩阵转换,GLKMatrixStack可以追踪矩阵变化的历史记录。简单的矩阵运算如下,复杂运算在以后示例中使用。
- (void)resizedWithWidth:(int)width height:(int)height {
glViewport(0, 0, width, height);
_mProgection = GLKMatrix4MakePerspective(GLKMathDegreesToRadians(60.0f), width/(float)height, 0.1f, 1000.0f);
GLKVector3 vLooking = GLKVector3Add(_cameraFrame.vWhere, _cameraFrame.vForward);
_mCamera = GLKMatrix4MakeLookAt(_cameraFrame.vWhere.x, _cameraFrame.vWhere.y, _cameraFrame.vWhere.z, vLooking.x, vLooking.y, vLooking.z, _cameraFrame.vUp.x, _cameraFrame.vUp.y, _cameraFrame.vUp.z);
GLKMatrix4 matrixMVP = GLKMatrix4Multiply(_mProgection, _mCamera);
GLKMatrix3 mNormal = GLKMatrix4GetMatrix3(_mCamera);
}
- (void)moveForwardWithDistance:(float)distance {
GLKVector3 vForward = GLKVector3MultiplyScalar(_cameraFrame.vForward, distance);
_cameraFrame.vWhere = GLKVector3Add(_cameraFrame.vWhere, vForward);
}
- (void)rotateLocalYWithAngle:(float)angle {
// 绕着镜头z轴向量创建一个旋转矩阵
GLKMatrix4 rot = GLKMatrix4MakeRotation(angle, _cameraFrame.vUp.x, _cameraFrame.vUp.y, _cameraFrame.vUp.z);
GLKVector3 vNewForward = GLKMatrix4MultiplyVector3(rot, _cameraFrame.vForward);
_cameraFrame.vForward = GLKVector3Normalize(vNewForward);
}
GLKit处理纹理主要有两个类。GLKTextureLoader负责加载2D或者3D纹理,并返回一个GLKTextureInfo的实例。另外GLKTextureLoader可以在子线程上使用Shared OpenGL Context异步加载纹理。GLKTextureInfo包含纹理大小,OpenGL Target Type等信息。
- (void)initModels {
NSString *path = [[NSBundle mainBundle] pathForResource:@"rock" ofType:@"png"];
NSError *error = nil;
_textureStones = [GLKTextureLoader textureWithContentsOfFile:path options:nil error:&error];
//convert png file to mipmaps
glBindTexture(GL_TEXTURE_2D, _textureStones.name);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);
}
name:纹理的标识符,使用glBindTexture()将其绑定到context中。
target:纹理的类型,只支持GL_TEXTURE_2D和GL_TEXTURE_CUBEMAP。
width; height; depth:以像素为单位的纹理宽、高、深度。
alphaState:保存Alpha通道在颜色数据中的存储方式,可选None、NonPremultiplied、Premultiplied三个类型
textureOrigin:源图片的原点位置,可以是Unknown、TopLeft、BottomLeft,这个属性由GLKTextureLoader设置,代表纹理在加载时是否需要镜像处理
containsMipmaps:Bool值,标识是否包含率纹理(Mipmaps)
mimapLevelCount:率纹理(Mipmaps)的等级数量
arrayLength:数组长度
4.4 Retina屏适配
在Mac Retina屏系列出现以后,对于Cocoa中视图的尺寸都是以point为单位,而OpenGL函数中图像及纹理的尺寸都是以pixel为单位。在非Retina屏幕中它们具有相同的值,但是在Retina屏幕中,它们具有不同的值。Mac OS对于OpenGL默认的渲染规则是1个点渲染1个像素,启用Retina特性的代码级两个单位之间的转换代码如下。
- (instancetype)initWithCoder:(NSCoder *)decoder {
if (self = [super initWithCoder:decoder]) {
// 启用全像素渲染
[self setWantsBestResolutionOpenGLSurface:YES];
}
return self;
}
- (void)reshape {
NSRect bounds = [self bounds];
// 将点数转化为像素
NSRect backRect = [self convertRectToBacking:bounds];
glViewport(0, 0, NSWidth(bounds), NSHeight(backRect));
}
4.5 窗口化程序
后面的例子都会基于活动窗口创建OpenGL程序,此时我们会使用窗口系统提供的默认缓存接受处理后的数据。在MacOS中存在一个桌面缓存合成引擎,系统屏幕包含一个大的帧缓存对象,桌面合成引擎会在合适的时机将系统各个可见窗口的帧缓存数据合成到屏幕帧缓存至少,最后将合成后的帧缓存渲染到屏幕上。桌面合成引擎只有收到缓存交换信号时才会处理窗口的帧缓存,发出缓存交换信号需要调用函数glFlush()。下图简单表示了窗口缓存和屏幕缓存的关系。
4.6 全屏渲染
很多OpenGL程序需要进行全屏渲染,而不是在一个活动窗口中。实现这个目标有两种方式,1)创建一个和整个屏幕一样大的窗口,在OS X10.6之前,这并不是最优的方式。2)使用CGL捕捉屏幕得到更好的全屏渲染效果。在OS X10.6之后,苹果不建议捕捉屏幕,当渲染到全屏幕窗口时,将会得到一个Context标记,OS X会屏幕捕捉方式使用的技术自动优化图像渲染。另外还可以使用双渲染缓存的方式即先将图像渲染至一个更小的缓存中来提升填充性能。捕捉屏幕还会导致UI信息无法在全屏时弹出(如系统提示电量不足信息)。
使用上述第一个方式实现全屏渲染时,在NSOpenGLView的子类中的渲染逻辑时,不再使用glFlush()
,取代它的是方法[[self openGLContext] flushBuffer]
。
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
[[self openGLContext] flushBuffer];
}
程序加载完成后手动实例化NSOpenGLView的方法如下。
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
NSRect mainDisplayRect = [[NSScreen mainScreen] frame];
NSWindow *fullScreenWindow = [[NSWindow alloc] initWithContentRect:mainDisplayRect
styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:YES];
[fullScreenWindow setLevel:NSMainMenuWindowLevel+1];
[fullScreenWindow setOpaque:YES];
[fullScreenWindow setHidesOnDeactivate:YES];
NSOpenGLPixelFormatAttribute pixelFormatAttributes[] = {
NSOpenGLPFAColorSize, 32,
NSOpenGLPFADepthSize, 24,
NSOpenGLPFAStencilSize, 8,
NSOpenGLPFAAccelerated,
NSOpenGLPFADoubleBuffer,
NSOpenGLPFAOpenGLProfile, NSOpenGLProfileVersion4_1Core, 0
};
NSOpenGLPixelFormat* pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:pixelFormatAttributes];
NSRect viewRect = NSMakeRect(0.0, 0.0, mainDisplayRect.size.width, mainDisplayRect.size.height);
GLCoreProfileView *fullScreenView = [[GLCoreProfileView alloc] initWithFrame:viewRect pixelFormat:pixelFormat];
[fullScreenWindow setContentView: fullScreenView];
[fullScreenWindow makeKeyAndOrderFront:self];
[fullScreenWindow makeFirstResponder:fullScreenView];
}
4.7 帧率同步
Macbook Pro渲染图像是从上至下逐行刷新,当渲染的帧率和屏幕刷新帧率不一致时,可能导致屏幕刷新到一半时,渲染新的图像,从而导致屏幕上下显示了两帧不同的图像。Macbook Pro的屏幕刷新率为60FPS,我们不能直接设置渲染帧率,但是可以通过设置每帧渲染之前的屏幕刷新率次数kCGLCPSwapInterval来控制渲染帧率。他可以消除图像撕裂现象。可以这样理解,当该值为2时,图像渲染帧率最大为30FPS,当一个渲染操作完成后,负责渲染的线程将会等待两次屏幕刷新时间后继续执行渲染操作。此时后台线程可以完成其他更重要的处理任务。在任何时候都可以调用改变渲染帧率的方法,它会立即生效。需要注意的是,这个并不能保证渲染率一定为30FPS,因为如果单个渲染逻辑超时,会发生丢帧现象。
GLint sync = 1;
CGLSetParameter(CGLGetCurrentContext(), KCGLCPSwapInterval, &sync);
4.8 提升填充效率
填充效率指的是将图像数据转换为帧缓存像素信息的速率。一个简单的办法就是使用更小的窗口,在全屏模式下降低屏幕分辨率。在OS X10.6之前,对于一个全屏幕OpenGL游戏,在运行游戏、捕捉屏幕等操作之前改变屏幕分辨率并不常见。捕捉屏幕并改变屏幕分辨率方法暂不深入研究。在使用双缓存的前提下,我们可以用CGL去改变协调图像缓存的尺寸,使其小于前台图像缓存来提升填充效率。当渲染到屏幕时,协调图像缓存的内容会拉伸填充到整个前台图像缓存。
在改变协调图像缓存尺寸时,必须重新设置glViewport。
- (void) reshape {
GLint dim[2] = {newWidth, newHeight};
CGLSetParameter(CGLGetCurrentContext(), kCGLCPSurfaceBackingSize, dim);
CGLEnable(CGLGetCurrentContext(), kCGLCESurfaceBackingSize);
glViewport(0, 0, newWidth, newHeight);
}
4.9 OpenGL多线程操作
OpenGL驱动会先通过CPU处理一部分计算,然后最终将其交付到GPU渲染。在OS X 10.5以后,可以启用多线程OpenGL核心将这些任务交给多个线程处理。在多核CPU设备上,这样能再一定程度上提升性能。但其提升效果有限,甚至有时会降低性能。当有关OpenGL执行的代码的执行性能不受限于CPU时,通常不会有明显的性能提升。另外,调用大量的生成渲染管道函数(glGetFloatv(), glGetIntegerv(), glReadPixels()等)也会影响其性能提升潜力。
CGLEnable(CGLGetCurrentContext(), kCGLCEMPEngine);
4.10 GLUT
GLUT的全称为OpenGL Utility Toolkit,一个基于窗口系统的OpenGL编程工具。最早于1994年发布,初期定位为说明和学习框架,随后不断扩展,支持基本的游戏编程特性。尽管现在不是主流的工具,但是Apple针对OS X进行了部分优化。
创建基于GLUT的程序需要移除空白项目中的AppDelegate.m/.h和main.m文件,并创建包含GLUT的C/C++文件。
int main(int argc, char* argv[]) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH | GLUT_STENCIL | GLUT_3_2_CORE_PROFILE);
glutCreateWindow("GLUT Core Profile Demo");
glutReshapeFunc(ChangeSize);
glutKeyboardFunc(KeyPressFunc);
glutDisplayFunc(RenderScene);
SetupRC();
printf("Version: %s\r\n", glGetString(GL_VERSION));
printf("Renderer: %s\r\n", glGetString(GL_RENDERER));
printf("Vendor: %s\r\n", glGetString(GL_VENDOR));
printf("GLSL Version: %s\r\n",
glGetString(GL_SHADING_LANGUAGE_VERSION));
glutMainLoop();
return 0;
}
4.11 渲染一个三角形
这里通过一个简单的例子来了解最基本的OpenGL程序组成,以及着色器语言编写方法。首先在Xocde中新建一个mac OS目录下的Cocoa App程序,在主窗口中添加NSOpenGLView的子类GLCoreProfileView,如前文描述。这里使用Shader.vsh和Shader.fsh文件分别用于编写顶点着色器和片段着色器。并且在工程中导入OpenGL和GLKit框架。完成后的文件目录如下所示。
一个基本的OpenGL程序包含以下几个步骤。
- 1-配置GLContext,这里配置为Apple支持的最新4.1 Profile,具体方法见前文。
- 2-准备着色器字符串,这里使用创建空白文件方式,其内容见下面代码。
- 3-创建、编译着色器。
- 4-创建、链接OpenGL程序(program)。
- 5-创建顶点数组对象(VAO),并将数据关联至着色器,这里只创建,关联在后面章节讲解。
- 6-渲染图形。
准备顶点着色器
// 指定了OpenGL版本为 4.1 Core Profile
#version 410 core
void main() {
// 通常顶点数据是通过外部传入,这里简单示例采用另外一种方式gl_VertexID,它检查外面调用的方法glDrawArrays(GL_TRIANGLES, 0, 3);
// 从第一个参数开始取顶点,直到最后一个参数定义的顶点数量。
// The gl_VertexID input starts counting from the value given by the first parameter of glDrawArrays()
// and counts upwards one vertex at a time for count vertices (the third parameter of glDrawArrays())
const vec4 vertices[3] = vec4[3](vec4( 0.25, -0.25, 0.5, 1.0),
vec4(-0.25, -0.25, 0.5, 1.0),
vec4( 0.25, 0.25, 0.5, 1.0));
// gl_Position为GLSL中的内部的输出变量,这个输出变量OpenGL内部会用于拓扑重建等任务(Tessellation)。
gl_Position = vertices[gl_VertexID];
}
准备片段着色器
#version 410 core
// Out表示输出变量,这个输出变量会被OpenGL捕捉,并用于图形的渲染
out vec4 FragColor;
void main() {
FragColor = vec4(0.0, 0.8, 1.0, 1.0);
}
创建和编译着色器、链接程序、创建VAO
- (void)prepareOpenGL {
[self loadShaders];
glGenVertexArrays(1, &_vertexArray);
glBindVertexArray(_vertexArray);
}
- (BOOL)loadShaders {
GLuint vertexShader;
GLuint fragShader;
NSString *vertexShaderPathName;
NSString *fragShaderPathName;
_program = glCreateProgram();
vertexShaderPathName = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"vsh"];
if (![self compileShader:&vertexShader type:GL_VERTEX_SHADER filePath:vertexShaderPathName]) {
NSLog(@"Failed to compile vertex shader");
}
fragShaderPathName = [[NSBundle mainBundle] pathForResource:@"Shader" ofType:@"fsh"];
if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER filePath:fragShaderPathName]) {
NSLog(@"Failed to compile fragment shader");
}
glAttachShader(_program, vertexShader);
glAttachShader(_program, fragShader);
if (![self linkProgram:_program]) {
NSLog(@"Failed to link program: %d", _program);
if (vertexShader != 0) {
glDeleteShader(vertexShader);
vertexShader = 0;
}
if (fragShader != 0) {
glDeleteShader(fragShader);
fragShader = 0;
}
if (_program != 0) {
glDeleteProgram(_program);
_program = 0;
}
return NO;
}
if (vertexShader != 0) {
glDetachShader(_program, vertexShader);
glDeleteShader(vertexShader);
}
if (fragShader != 0) {
glDetachShader(_program, fragShader);
glDeleteShader(fragShader);
}
return YES;
}
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type filePath:(NSString *)path {
const GLchar *shaderSource = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil].UTF8String;
*shader = glCreateShader(type);
glShaderSource(*shader, 1, &shaderSource, nil);
glCompileShader(*shader);
GLint status = 0;
glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
if (status == 0) {
GLint logLen = 0;
glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLen);
GLchar *infoLog = malloc(sizeof(char) * logLen);
glGetShaderInfoLog(*shader, logLen, NULL, infoLog);
NSLog(@"Shader at: %@", path);
fprintf(stderr, "Info Log: %s\n", infoLog);
glDeleteShader(*shader);
return NO;
}
return YES;
}
- (BOOL)linkProgram:(GLuint)program {
glLinkProgram(program);
GLint status = 0;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status == 0) {
GLint logLen = 0;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &logLen);
GLchar *infoLog = malloc(sizeof(char) * logLen);
glGetProgramInfoLog(program, logLen, NULL, infoLog);
fprintf(stderr, "Prog Info Log: %s\n", infoLog);
return NO;
}
return YES;
}
渲染图形
- (void)drawRect:(NSRect)dirtyRect {
const GLfloat color[] = { (float)sin(_lifeDuration) * 0.5f + 0.5f, (float)cos(_lifeDuration) * 0.5f + 0.5f,
0.0f, 1.0f };
glClearBufferfv(GL_COLOR, 0, color);
// 当glDrawArrays画顶点时可以指定点的大小
// glPointSize(40);
glUseProgram(_program);
glDrawArrays(GL_TRIANGLES, 0, 3);
glFlush();
}
上面的代码绘制的结果如下图所示。Demo传送门
5 总结
在这个短文中,我们主要聊到了两个关键点,(1)OpenGL的图像管道,和(2)MacOS开发环境搭建。