翻译:关于OpenGL ES 2.x(第三篇)
翻译:关于OpenGL ES 2.x(第三篇)
原文:All about OpenGL ES 2.x - (part 3 / 3)
我的朋友们,欢迎回来!
这将是本系教程的最后一篇。
是时候去挖掘一些关于OpenGL和3D世界更高级的知识了。在这个教程中我们看到很多关于2D图形,多采样,纹理,离屏渲染,让我们尝试最大优化应用的性能。
关于前面两篇教程中提到的所有概念你都要知道,这是非常重要的,如果你忘了,下面有链接可以查看。
这个系列由3部分组成:
这里我假设你已经知道很多关于OpenGL和3D世界的东西。你可能已经用OpenGL创建了一些应用,发现很多非常酷的事情,可能也发现一些问题,甚至可能你自己的引擎/框架正在构建中,我很高兴看到你回来。
正如我在一本书中看到的一句话:有一天你不能行走。然后你学习怎样站立和行走。现在就去奔跑、跳跃和遨游吧,但为什么不飞呢?使用OpenGL我们的想象不会受到约束,我们可以自由翱翔。
让我们开始吧
这里有一个小目录,方便你阅读:
前言
记住下面的每点
- OpenGL的逻辑由3个简单的概念构成:原语、缓冲区和栅格化
- OpenGL 2.x工作原理是可编程管线,也就是着色器的同义词
- OpenGL不关心输出的设备,平台或者显示的屏幕。为了桥接OpenGL与我们的设备,我们必须使用EGL(或iOS中的EAGL)
- 纹理是非常重要的,要指定一个像素格式和顺序,以适应OpenGL
- 我们通过调用
glDraw*
开始渲染过程。首先将通过顶点着色器,一些检查将得出结论,即处理后的顶点是否可以进入片段着色器- 我们网格的原始结构永远都不会改变。我们只是创建转换矩阵以产生所需的结果
首先我们将讨论2D图形,然后让我们看看什么是多重采样/抗锯齿滤波器,我个人不喜欢这种技术带来的未知收益。很多情况下,无需多重采样可以很好的运行应用程序,但是简单的多重采样过滤器就可能会完全破坏该应用程序的性能。总之,有时我们需要临时的多重采样来产生平滑的图像。
稍后我们将深入讨论纹理及其优化,即每个像素2字节的数据格式。同时也看看PVRTC和怎样放入OpenGL纹理,以及渲染到屏幕外的表面。
最后,我将简要介绍一下我自己发现的一些性能收获。一些提示和技巧对今天的我仍有很大的帮助,也与你们分享。让我们开始吧!
OpenGL 2D图形
将2D图形与OpenGL配合使用并不一定限于使用线或点图元。三个基本原语(三角形、线和点)很好的被用于2D和3D。关于2D图形首要的是
Z
深度。我们所有的工作将在2维上,不包括Z
轴平移和绽放,也不包括X
和Y
轴的旋转。那表明我们不需要使用深度渲染缓冲区,因为我们所有的绘制将会在同一个Z
的位置(通常是0.0)。随之而来的一个问题:OpenGL怎么知道哪个将被绘制在另一个的前面(或者上面)?非常简单,通过我们想要绘制图像的顺序,在后面的要被先绘制。OpenGL也提供了一个功能,被称做:多边形偏移量,但它更像是一种调整,而不是实际的排序。现在我们从三个方面思考2D图形:
- 在相同的
Z
位置有很多正方形- 在相同的
Z
位置有很多点- 以上两者均是
你可以想象对于有百万个三角形状态机的OpenGL处理几个三角形是多么容易的一件事。在极端情况下,2D图形工作在几百个三角形。简而言之,所有都是纹理。因此大部分2D工作将是在纹理上。许多人感觉被迫创建API可以与非2的次幂(Non-POT)纹理一起使用,那就是说要使用3618、5139等尺寸的纹理。我的建议是:不要那样做!工作在不是2的次幂的纹理不是一个好的想法。在上面的图片中,使用虚构的网格(应该是POT)总是一个好的主意,使用1616,3232将是一个好的选择。如果你计划用PVRTC压缩图片文件,应该使用一个88的格子,因为PVRTC最小的尺寸是88。不建议使用比88小的尺寸,没有必要提高精度,也会增加开发工作,还会损耗应用的性能。88的格子精度已足够,我们将会很快看到网格之间的差异以及何时、如何使用它们。让我们再多谈一些网格的知识吧。
网格概念
我认为这是2D程序规划中最重要的一部分。例如,在3D游戏中,决定一个角色能否走动必须创建一个碰撞检测器。检测器可以是一个盒子(边界盒子)或者网格(边界网格,是原始网格的简单副本)。在两种情况下,计算是非常重要且昂贵的。但在2D应用中如果你使用网格找碰撞区域是非常容易的,因为你只使用一个正方形区域
X
、Y
坐标。这仅只是一个原因,因为网格非常重要。我知道你可以提出网格的许多其它的优点,例如组织,计算精度,屏幕上对象的精度等。在十年前或更久,我制作了一个小程序,用2D图形生成RPG游戏。网格的想法在那里就已经很成熟了。下面的图片展示一切是怎样放入网格中的:
让我们关于网格的重要特性。你可以点击旁边的图像放大它。首先想你注意到覆盖在图片上东西。注意到左边窗口是保留的一个库。在库的顶部可以看见一个32*32的小正方形。这些正方形保留在场景的底部(在我们的OpenGL语言中,将是背景)。库中的其它图片都是透明图片(PNG),可以被放在底部正方形的上面。通过查看网格上的大树,你可以看到这种差异。在格子上寻找“英雄”。他在靠近一颗树的右边,在一个盒子中带着红色的帽子,脸较小。这是关于网格的第二个要点。那个小人不只占有一个正方形网格,可以更大,但对于网格来说,动作分割符仅代表一个正方形。不懂?通常仅用一个网格的正方形处理动作是一个好主意,因为这样让你的代码组织起来比其它方法要好很多。创建动作区域,你可以复制动作的正方形,就像图片网格中右上角的区域的村庄出口。我确定在你的2D应用中创建一个控制类处理动作,然后创建一个视图类引用控制类是一件很容易的事,那么你可以准备视图类在网格的正方形获取碰撞检测。因此,你有1个动作-N网格方格检测器。这样你可以利用网格的所有优势,并可以极大地提高应用程序的性能。通过使用网格可以轻松定义角色无法穿过的碰撞区域,例如墙壁。另一个使用网格的优势是定义“顶部区域”,也就是,总是绘制在最上面的区域,如上面的树。如果角色穿过这些区域,将展示在后面。下面的图片是使用网格所有的概念展示的最终场景。注意有多少可以覆盖在图片上面,注意角色如何处理动作方块及顶部区域。同时要注意重叠所有内容最上面的效果,像云朵的阴影或太阳的光照。
总结要点就是:网格是规划2D应用的最重要的部分。网格在OpenGL并不是真实的事物,所以你要特别小心使用这个概念,所有都可以被想象。好吧,为了让你知道更多的信息:网格概念非重要,以至于OpenGL在内部与网格概念协同工作来构造片段。非常好,这是关于网格的所有。现在你会说:好的,但这不是我想要的,我想要一款像《暗黑破坏神》、《罪恶之城》甚至《We Rule》那样的投影游戏。哦,对了,让我们把事情变得更复杂,然后将深度渲染缓冲区和摄像机带回到2D应用中。
2D深度渲染缓冲区
了解2D图形如何与OpenGL配合使用,我们可以考虑采用更精细的方法,甚至像在2D应用中使用深度缓冲区。
点击上面的图片,你可注意到他们的不同处。都是来自著名的iOS游戏的截屏,都是使用OpenGL和都是2D游戏。尽管他们使用OpenGL ES 1.1,我们可以理解网格和深度缓冲区的概念。下面的游戏(Gun Bros)使用非常小的网格,正好是88的像素,此类型的网格为游戏提供了难以置信的精确度,可以将对象放在网格上,为了提高用户体验,需要制作一个网格集合处理动作,在这种情况下,一个不错的选择是为每个动作检测器,安排4或8个网格正方形。上面的游戏叫做Inotia,现在是第三个版本。从第一个版本开始,Inotia一直使用大网格3232像素的。也使用的是OpenGL ES1.1.这两种像素(88,3232)网格有许多不同的地方。8*8更精确,似乎也是更好的选择,但请记住这个选择增加了太多的处理。Inotia游戏处理的需求不高,这对iOS的硬件绝对没有吸引力。你需要做一个更好的选择去适配规划使用的应用。现在我们讨论下深度渲染缓冲区,最棒的是你可以在应用程序中使用3D模型。看起来,没有深度渲染缓冲区,你只能使用带有纹理的正方形或其它原始几何形式。通过这种方式,你只能为每个位置动画信息创建不同的纹理,尤其是角色动画,明显一个更棒的主意是利用纹理图集:
上面Inotia游戏的图片在游戏中出现的角色有相似的纹理图集。看上面的图你可以发现在屏幕上的三个角色只能向四个方向。现在再看一下上面的Gun Bros的图像。注意到角色可以你任意方向转动。为什么?很好,通过使用深度渲染缓冲区可以在2D应用中自由的使用3D模型。因此你可以根据网格和2D概念旋转、缩放和移动3D模型(无Z轴平移)。结果要好得多,但是如果进行任何改进,则与2D正方形相比会产生很大的性能成本。但有另一个重要的事情关于混合3D与2D的概念:相机的使用。你可以在Z轴上创建一个大平面,像在3D应用程序中一样放置对象,并创建具有正交投影的相机,而不是在屏幕前面立即创建一个平面,无需锁定Z轴平移。你还记得和如何去做么?点击这里,关于相机。在2D图形深入讨论相机和深度缓冲区前,知道这一点并没有真正的区别是非常重要的,在代码层面,2D和3D图形所有的东西都来自你自己的计划和组织。因此使用深度缓冲的代码与第二篇教程一样。现在让我们讨论2D图形使用相机。
2D相机
如你已经看过这篇关于相机的教程(camera tutorial)现在我确定你知道怎样去创建相机和正交矩阵。那现在有一个问题:在2D图形中使用相机和深度渲染缓冲最佳的地方和方法是哪里?下面的图片说明胜过千言万语:
此图展示了一个类似Diablo游戏的场景,使用相机在相似的位置。你可以在两个矩阵清晰的看到不同之处。注意图片上的红线,在正交投影中,这些线是平行的,但是在透视投影中,这些线不是真正的平行,可以在无穷远处相交。现在让我们集中注意边在左下角灰色绽放的图片上,那是有物体的场景。正如你所见,那是真的3D物体,但在正交矩阵下你可以创建场景如Diablo、Sim City、Starcraft、其它畅销书之类的场景,为你的3D应用程序提供2D外观。如果你用另一个视角看Gun Bros游戏的图片,你会发现看到的正是他们所做的,其中有一个带正交投影的相机和放置在场景中的真实3D对象。在你期望的位置创建相机的方法是在3D世界构建你所有的场景,用正交矩阵设置相机,同时通过使用网格引导你的空间。
关于这个主题我还有最后一个建议,确切说是一个警告。透视和正交矩阵是完全不一样的。因此相同的焦点、视角、近端和远端相同配置会产生完全不同的结果。因此,你需要为本要使用正交投影配置,而不是使用透视投影配置。可能你使用透视矩阵正常显示,使用正交矩阵你可能看不见任何东西。这不是错误,这是透视与正交计算间和不同之处。关于OpenGL的2D图形最重要的概念,让我们来复习下:
- OpenGL有两种方式使用2D图形:使用或者不用深度渲染缓冲
- 不使用深度渲染缓冲,可以在屏幕上创建类型矩形的任何东西,这种方式忘记Z轴位置。你在这里的工作将非常繁琐。尽管通过这种方式,可以在OpenGL中获得最佳性能。
- 使用深度渲染缓冲,可以使用真正的3D物体,同时可能你想使用一个相机和正交矩阵。
- 你选择独立的方法,当使用2D图形时,通常使用网格概念。这是组织你的世界和优化应用性能的最好方法。
现在是时候回到3D世界,讨论些关于多采样和抗锯齿滤波。
多采样
我确定你已经注意到所有实时渲染的3D应用,他们的边缘都是有锯齿。我们通常讨论的3D世界,如3D软件或游戏,无论如何,边缘总是看起来像是走样的(我的意思是,在大多数情况下)。出现这种情况的原因是因为缺乏完善的技术来适应这种情况,这是因为我们的硬件功能还不强大,无法实时处理像素融合。所以我首先想说的是抗锯齿滤波是非常耗性能的。在大多数的情况下这点小问题(边缘锯齿)是没什么关系的。但在某些情况下你的3D应用需要看起来更好。最简单和常用的是在3D软件中的渲染。当我点击3D软件的渲染按钮时,我们期望看见精美的图像,而不是边缘锯齿。简而言之,OpenGL原语获取网格的栅格(类似网格概念),他们的边缘变得变形。OpenGL ES 2.0支持一些功能被称为多采样。它是一个抗锯齿滤波的技术,一个像素被分成几个样本,每个这样的样本被当做一个最小的像素栅格化处理。每个取样有自己的颜色、深度和模板的信息。当你在帧缓冲区询问OpenGL最终的图像时,它将会解析和混合所有取样。这些处理将生成更顺滑的边缘。OpenGL ES 2.0通常配置抗锯齿技术,即使取样的数值是1,也就是说1像素=1样本。理论上看起来很简单,但请记住OpenGL不知道任何关于设备的屏幕像素和颜色。桥接OpenGL与设备是通过EGL。所以设备的颜色信息,像素信息和屏幕信息由EGL负责,因此多采样不能仅仅由OpenGL实现,它需要插件,由供应商负责。每个供应商必须创建一个插件,以指示必要信息,这样做OpenGL才能真正解析出多重采样。默认的EGL API提供多重采样的配置,但实现者通常会做些改变。在Apple的实现中,插件被称作:“Multisample APPLE”,且放在OpenGL扩展的头文件中(glext.h)。为了正确实现苹果多重采样,你需要2个帧缓冲区和4个渲染缓冲区。OpenGL通常提供1个帧缓冲区,另一个是抗锯齿缓冲区。渲染缓冲区是颜色和深度。在glext.h中有三个新的函数处理苹果多重采样:
GLvoid glRenderbufferStorageMultisampleAPPLE(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height)
* target: OpenGL内部约定值一般为`GL_RENDERBUFFER`
* samples: 多重滤波采样的数值
* internalformat: 指定想要的类型的渲染缓冲和临时图像颜色格式。此值可能为:
* GL_RGBA4,GL_RGB5_A1,GL_RGB56,GL_RGB8_OES或GL_RGBA8_OES最终的渲染颜色
* GL_DEPTH_COMPONENT16或GL_DEPTH_COMPONENT24_OES渲染缓冲Z轴深度
* width: 渲染缓冲最终的宽度
* heigth: 渲染缓冲最终的高度
GLvoid glResolveMultisampleFramebufferAPPLE(void)
* 此函数不需要传入任何参数。此函数将解析分别绑定到GL_DRAW_FRAMEBUFFER_APPLE和GL_READ_FRAMEBUFFER_APPLE的最后两个帧缓冲区
GLvoid glDiscardFramebufferEXT(GLenum target, GLsizei numAttachments, const GLenum *attachments)
* target: 通常值为GL_READ_FRAMEBUFFER_APPLE
* numAttachments: 要在目标缓冲区中丢弃的渲染缓冲区附件数。通常为2,以丢弃颜色和深度渲染缓冲区
* attachments: 指向数组的指针,包含要丢弃渲染缓冲区的类型。数组值通常为 {GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT}
在检查代码前,让我们更多的理解这些新的函数。第一个函数
glRenderbufferStorageMultisampleAPPLE
是想要代替将该属性设置为渲染缓冲区的函数glRenderbufferStorage
。此函数最大的改变是取样数设置,将定义每个像素拥有多少取样。第二个函数glResolveMultisampleFramebufferAPPLE
被用来从原始帧缓冲区获取信息,放入多体采样帧缓冲区,解析每个像素的取样,然后再次绘制解析的图片到原始的帧缓冲区。一句话:这是Multisample APPLE的核心,这就是此函数做的所有事。最后一个函数glDiscardFramebufferEXT
是另一个清除函数。如你所想,在glResolveMultisampleFramebufferAPPLE
函数之后做了所有处理,多采样帧缓冲区将拥有很多信息,要清除所有内存。为了实现这个,我们可以调用glDiscardFramebufferEXT
通知我们想从哪里开始清除。下面是一个使用Multisample APPLE的完整代码:
// EAGL
// Assume that _eaglLayer is a CAEAGLLayer data type and was already defined.
// Assume that _context is an EAGLContext data type and was already defined.
// Dimensions
int _width, _height;
// Normal Buffers
Gluint _frameBuffer, _colorBuffer, _depthBuffer;
// Multisample Buffers
Gluint _msaaFrameBuffer, _msaaColorBuffer, _msaaDepthBuffer;
int _sample = 4; // This represents the number of samples
// Normal Frame Buffer
glGenFramebuffers(1, &_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
// Normal Color Render Buffer
glGenRenderbuffers(1, &_colorBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorBuffer);
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable: _eaglLayer];
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBuffer);
// Retrieves the width and heigth to the EAGL Layer, just necessary if the width and height was not informed.
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_height);
// Normal Depth Render Buffer
glGenRenderbuffers(1, &_depthBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _depthBuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, _width, _height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer);
glEnable(GL_DEPTH_TEST);
// Multisample Frame Buffer
glGenFramebuffers(1, &_msaaFrameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _msaaFrameBuffer);
// Multisample Color Render Buffer
glGenRenderbuffers(1, &_msaaColorBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _msaaColorBuffer);
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, _samples, GL_RGBA8_OES, _width, _height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _msaaColorBuffer);
// Multisample Depth Render Buffer
glGenRenderbuffers(1, &_msaaColorBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _msaaDepthBuffer);
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, _samples, GL_DEPTH_COMPONENT16, _width, _height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _msaaDepthBuffer);
是的,一个基本的配置。一旦这些6个缓冲被定义了,我们也要通过不同的方法创建渲染。下面是必要的代码:
// Rendering with Multisample APPLE
.
// -------------------------
// Pre-Render
// -------------------------
// Clear normal Frame Buffer
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// Clear multisample Frame Buffer
glBindFramebuffer(GL_FRAMEBUFFER, _msaaFrameBuffer);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// -------------------------
// Drawing
// -------------------------
// ...
// Draw all your content
// ...
// -------------------------
// Render
// -------------------------
// Resolving Multisample Frame Buffer
glBindFramebuffer(GL_DRAW_FRAMEBUFFER_APPLE, _frameBuffer);
glBindFramebuffer(GL_READ_FRAMEBUFFER_APPLE, _msaaFrameBuffer);
glResolveMultisampleFramebufferAPPLE();
// Apple (and the khronos group) encourages you to discard
// render buffer contents whenever is possible.
GLenum attachments[] = {GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT};
glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER_APPLE, 2, attachments);
// Presents the final result at the screen.
glBindRenderbuffer(GL_RENDERBUFFER, _colorBuffer);
[_context presentRenderbuffer:GL_RENDERBUFFER];
如果你想记住关于EAGL的一些东西(由APPLE实现的EGL),查看这里:EGL&EAGL。OpenGL同样也提供一些配置给多采样,
glSampleCoverage
和用glEnable
的一些配置。在这里我不深度讨论这些配置,因为我相信多采样不值得我们在这上面花时间。就像我告诉你的,结果并不重要,只是稍微完善了一点。我认为,与最终结果相比,性能成本过高。好的,现在是时候更多讨论OpenGL的纹理。
更多关于纹理
从此系列的第二部分我们已经了解很多的关于纹理知识。首先,我们讨论优化的类型。对我们应用的性能有很大的提高和非常容易实现。我们即将讨论图像每像素的字节数。
每像素字节数
通常,图像每像素有4字节,每个通道一个字节(RGBA)。有此图片没有alpha通道,如jpg格式,每像素只有3个字节(RGB)。每个字节可以由一个16进制的颜色格式0XFF来表示,被称作16进制,是因为个小数位的范围是0-F,当你把两个16进制的数结合可以得到一个字节(16*16=256)。通常,我们描述用16进制0XFFFFFF代表一个颜色,每两个数字代表一个颜色通道(RGB)。对于有alpha通道的图片,如png格式,我们习惯用0xFFFFFF+0xFF,也就是(RGB+A)。我的下篇文章将是关于二进制编程,所有这里不深入谈论二进制。这里我们需要知道1字节=1颜色通道。OpenGL也可以工作在更多的压缩的格式,如每个像素用2个字节。那是什么意思?代表每个字节存储2个颜色通道,包括alpha。简而言之,减少图片的颜色范围。OpenGL为我们提供了3个压缩的数据类型:
GL_UNSIGNED_SHORT_4_4_4_4
、GL_UNSIGNED_SHORT_5_5_5_1
、GL_UNSIGNED_SHORT_5_6_5
。前面两个使用在有alpha通道场景,最后一个只用在没有alpha通道的情况下。这3个名字告诉一些关于像素数据。在右边数字指示我们数字的位(不是字节)只准用在每个通道(RGBA)。为了使它更清晰,每个字节由8位组成。在第一和情况下每个像素2个字节,每个通道由4位。第二个,5位给RGB通道,1位给alpha通道。最后一个,每像素由2字节组成,5位给R,6位给G,5位给B。这里有个小警示:GL_UNSIGNED_SHORT_5_5_5_1
此类型不是很有用,因为只有1位表示alpha通道,等同于给一个布尔值一样,YES可见,反之不可见。因此这个类型没什么大用,与后一个类型比较绿色通道位数要少,也不能产生真正的你前一个产生透明效果。如果你需要alpha通道用前面一个,不需要就用后面一个。关于最后一个类型的一点小注意。人类的眼睛对绿色更敏感,所以绿色通道有更多的位数。用这种方式,即使更少的颜色范围,解析的最终图片看起来也不会太不同。好,让我们看下两和压缩间的不同。
如你所见,使用
GL_UNSIGNED_SHORT_4_4_4_4
类型在某些情况下看起来确实很丑。但GL_UNSIGNED_SHORT_5_6_5
类型就非常好。为什么?我将在下一篇有关二进制的文章中详细解释,一句话,使用GL_UNSIGNED_SHORT_4_4_4_4
每个通道只有16个色调,包括alpha。但使用GL_UNSIGNED_SHORT_5_6_5
红色和蓝色有32个色调,以及96种绿色频谱。它仍然远远超出人眼的能力,但请记住,通过使用这些优化,我们在所有图像中每像素减少2个字节,这代表了渲染的更多性能。现在是时候学习如何将传统图像转换到这些格式。通常,从一张图像中按照一个像素一个像素提取二进制信息,因此每个像素将由一个unsigned int
数据类型4个字节组成。每个编程语言提供了一个方法从像素提取二进制信息。拥有像素数据数组(unsigned int数组)后,可以使用以下代码将该数据转换为GL_UNSIGNED_SHORT_4_4_4_4
或GL_UNSIGNED_SHORT_5_6_5
。
// Converting 4bpp to 2bpp
.
typedef enum
{
ColorFormatRGB56,
ColorFormatRGBA4444,
} ColorFormat;
static void optimizePixelData(ColorFormat color, int pixelDataLength, void *pixelData)
{
int i,
int length = pixelDataLength;
void *newData;
// Pointer to pixel information of 32 bits (R8 + G8 + B8 + A8)。
// 4 bytes per pixel.
unsigned int *inPixel32;
// Pointer to new pixel information of 16 bits(R5 + G6 + B5)
// or (R4 + G4 + B4 + A4)
// 2 bytes per pixel
unsigned short *outPixel16;
newData = malloc(length * sizeof(unsigned short));
inPixel32 = (unsigned int *)pixelData;
outPixel16 = (unsigned short *)newData;
if(color == ColorFormatRGB565)
{
// Using pointer arithmetic, move the pointer over the original data.
for(i = 0; i < length; ++i, ++inPixel32)
{
// Makes the convertion, ignoring the alpha channel, as following:
// 1 - Isolates the Red Channel, discards 3 bits(8 - 3), then pushes to the final position
// 2 - Isolates the Green Channel, discards 2 bits(8 - 2), then pushes to the final position
// 3 - Isolates the Blue Channel, discards 3 bits(8 - 3), then pushes to the final position
*outPixel16++ = ((((*inPixel32 >> 0) & 0xFF) >> 3) << 11) | ((((*inPixel32 >> 8) & 0xFF) >> 2) << 5) | ((((*inPixel32 >> 16) & 0xFF) >> 3) <<0);
}
}
else if (color == ColorFormatRGBA4444)
{
// Using pointer arithmetic, move the pointer over the original data.
for(i = 0; i < length; ++i, ++inPixel32)
{
// Makes the convertion, as following:
// 1 - Isolates the Red channel, discards 4 bits (8 - 4), then push to the final position.
// 2 - Isolates the Green channel, discards 4 bits (8 - 4), then push to the final position.
// 3 - Isolates the Blue channel, discards 4 bits (8 - 4), then push to the final position.
// 4 - Isolates the Alpha channel, discards 4 bits (8 - 4), then push to the final position.
*outPixel16++ = ((((*inPixel32 >> 0) & 0xFF) >> 4) << 12) | ((((*inPixel32 >> 8) & 0xFF) >> 4) << 8) | ((((*inPixel32 > 16) & 0xFF) >> 4) << 4) | ((((*inPixel32 >> 24) & 0xFF) >> 4) << 0);
}
}
free(pixelData);
pixelData = newData;
}
上面的示例假定通道顺序为RGBA。虽然不通用,你的图片的像素组成可能用另一种通道顺序,如ARGB或BGR。在这些情况下,你要改变上面示例或在从像素提取二进制信息时改变通道顺序。另一个重要的事情是关于二进制顺序。如果你不是很了解二进制,我不小迷惑你的思维,只是一个建议:通常,你很有可能拿到的像素数据是以小端格式,但如果你的编程语言获得的二进制信息以大端格式,上面的示例就不能正常工作,因此要确保你的像素数据是小端格式。
PVRTC(PowerVR Texture Compression)
我相信你已经听过纹理压缩格式PVRTC,如果你对此主题已经感到满意,则跳到下一个。PVRTC是一个二进制格式,由图像技术创建(也可叫做Imgtec)。这个格式的通道顺序用ARGB代替传统的RGBA。说实话,如果仅从大小来看,它的优化不是关于文件大小,任何jpg压缩程度更高,甚至png也是有压缩。PVRTC优化它的处理过程,它的像素可以以每像素2字节(2bpp)或每像素4字节(4bpp)。PVRTC中的数据对OpenGL友好的,还可以存储Mipmap级别。那一直使用PVRTC是一个好的主意么?事实不是这样的,让我们看看为什么。在OpenGL ES 2.0中默认是不支持PVRTC格式,有消息OpenGL ES 2.1将会原生的支持PVRTC纹理,但现在是OpenGL ES 2.0。为使用PVRTC,仅作为多采样,你需要一个三方插件。对于Apple,此插件有四个新的常量。OpenGL提供一个函数从压缩格式,如PVRTC,上传像素数据:
// Uploading PVRTC
GLvoid glCompressedTexImage2D(GLenum target, GLint level, GLenum internalformat, GLsizer width, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const GLvoid *data)
* target: 值总是GL_TEXTURE_2D,OpenGL内部约定
* level: 文件中Mipmap级别
* internalformat: PVRTC格式,此参数可能为:
* GL_COMPREDDED_RGB_PVRTC_2BPPV1_IMG:文件使用没有alpha通道的2bpp
* GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG:文件使用有alpha通道的2bpp
* GL_COMPREDDED_RGB_PVRTC_4BPPV1_IMG:文件使用没有alpha通道的4bpp
* GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG:文件使用有alpha通道的4bpp
* width: 图像的宽度
* heigth: 图像的高度
* border:此参数在OpenGL ES中忽略。值为0。内部保留常量用来兼容桌面版本
* imageSize: 二进制数据的字节数
* data:图像的二进制数据
如你所想,数据格式
GL_UNSIGNED_SHORT_4_4_4_4
或GL_UNSIGNED_SHORT_5_6_5
的选择是基于文件格式,根据2bpp或者4bpp的RGB或RGBA。生成PVRTC有很多的选择。最常用的两个是:Imgtec和Apple的纹理工具。点击这里可以找到Imgtec Tools。Apple的工具在iPhone SDK中,位置在:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/下的名字为texturetool,你可以在Apple Texture Tool找到所有信息。我将解释怎么使用Apple工具。按下面的步骤:
- 打开终端(通常在/Applications/Utilities/Terminal.app)
- 选中 texturetool 拖到终端里。当然也可以用全路径访问/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/texturetool,我比较喜欢用拖拽的方式。
- 在纹理工具路径后面写上: -e PVRTC --channel-weighting-linear --- bits-per-pixel-2 -o
- 你需要写输出路径,我推荐用从Finder中拖拽文件到终端窗口并重全名扩展。扩展名无所谓,但我建议写一个可以表明身份的文件格式,如Chnnel Weighting Linear with 2bpp的简写:pvrl2
- 最后,加一个空格并写入输入文件。同样是从Finder中拖拽会更好。输入文件必须是PNG或JPG
- 按下回车键
// 示例
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/texturetool -e PVRTC --channel-weighting-linear --bits-per-pixel-2 -o /Texture/Output/Path/Texture.pvrl2 /Texture/Input/Path/Texture.jpg
现在你有一个PVRTC文件。问题是Apple工具不会生成传统的PVRTC二进制文件的头。在文件的开始的地方由52个字节组成,给出关于图像的宽高,Mipmap的数量,bpp,通道顺序,alpha值等等。在传统的PVRTC文件中,头的格式为:
- unsigned int (4 bytes): 字节表示的头的长度。老的PVRTC头是44字节而不是52
- unsigned int (4 bytes): 图像高度。 PVRTC仅接收正方形图像(宽 == 高)和2次幂的大小
- unsigned int (4 bytes): 图像宽度。 PVRTC仅接收正方形图像(宽 == 高)和2次幂的大小
- unsigned int (4 bytes): Mipmap的数量
- unsigned int (4 bytes): 标志
- unsigned int (4 bytes): 图像数据的长度
- unsigned int (4 bytes): bpp
- unsigned int (4 bytes): Red 位掩码
- unsigned int (4 bytes): Green 位掩码
- unsigned int (4 bytes): Blue 位掩码
- unsigned int (4 bytes): Alpha 位掩码
- unsigned int (4 bytes): PVR 标记
- unsigned int (4 bytes): 曲面数量
但是,使用Apple纹理工具,我们没有文件头,没有文件头就不能从代码中知晓文件的宽、高信息。因此使用Apple工具得到的PVRTC你需要知道bpp、宽、高及alpha。有点烦人,不是吗?好吧,我有个好消息要告诉你。我发现一个方法,一个小技巧,从Apple工具生成的PVRTC中提取信息。这个技巧可以正常工作,但无法识别有关Mipmap的信息,但这不是问题,因为Apple工具没有生成任何Mipmap。
// Extracting Infos From PVRTC Without Header
.
// Support the bpp of the image is 4, calculates its squared size
float size = sqrtf([data length] * 8 / 4);
// Checks if the bpp is really 4 by comparing the reset of division by 8,
// the minimum size of PVRTC, if the rest is zero the this image really
// has 4 bpp, otherwise, it has 2 bpp
bpp = ((int)size % 8 == 0) ? 4 : 2;
// Knowing the bpp, calculates the width and height
// based on the data size
width = sqrtf([data length] * 8 / bpp);
heigth = sqrtf([data length] * 8 / bpp);
length = [data length];
.
由TextureTool生成的PVRTC文件没有头文件,因此它的图像数据从文件的第一个字节开始。你可能会问一下那alpha呢?好吧,alpha将更多的取决于你的EAGL上下文的配置。如果你使用RGBA8,假定alpha存在,使用GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG或GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG,根据从上面的代码中获取的信息。如果你上下文使用RGB565,则设想使用GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG 或 GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG。现在在OpenGL ES 2.0上使用PVRTC,非常简单,你不需要改变任何东西,你将正常创建你的纹理,只用将glTexImage2D换成glCompressedTexImage2D函数。
// Uploading PVRTC to OpenGL
.
// format = one of the GL_COMPRESSED_RGB* constants.
// width = width extract from the code above.
// height = height extract from the code above.
// length = length extract from the code above.
// data = the array of pixel data loaded via NSData or any other binary class
// You probably will use NSData to load the PVRTC file.
// By using "dataWithContentsOfFile" or similar NSData methods.
glCompreddedTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, length, data);
.
做得好,这一切关于PVRTC。但关于这个主题的最后一点建议是:尽可能避免使用PVRTC。未知的成本收益不是很好。记住,对于OpenGL一张图片你仅需解析一次,因此PVRTC没有提供更大的优化。
离屏渲染
直到现在,我们讨论的是关于在屏幕上渲染,在设备上渲染,但我们还有另一个要渲染表面,即屏幕外的表面。你还记得EGL文章,对不对?EGL&EAGL
屏幕外的渲染的用途是什么?我们可以从当前帧拍摄快照并另存为一张图片,但对于离屏渲染最重要的是当前帧创建一个OpenGL纹理,然后用这个新的内部纹理制作反射贴图,即实时反射。我不会在这里谈论反射,这个主题更适合一个特定的教程,关于着色和光照,让我们只关注于渲染离屏的表面。我们需要知道一个新的函数:
// Off-Screen Render
GLvoid glFramebufferTexture2D(GLenum target, GLenum aatachment, GLenum textarget, GLuint texture, GLint level)
* target: OpenGL内部约定的值,总是GL_FRAMEBUFFER
* attachment: 指定我们想要渲染缓冲区的类型,参数值可为:
* GL_COLOR_ATTACHMENT0 颜色渲染缓冲区
* GL_DEPTH_ATTACHMENT 深度渲染缓冲区
* textarget: 纹理类型,对于2D纹理,此参数值是:GL_TEXTURE_2D,如果是3D纹理(立方体映射),可以使用其中一个面作为此参数值:
GL_TEXTURE_CUBE_MAP_POSITIVE_X,
GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
* texture: 纹理对象目标
* level: 为纹理指定Mipmap级别
使用这个函数我们需要首先创建目标纹理。我们可以像前面一样。然后调用glFramebufferTexture2D并正常执行渲染例程。在绘制一些东西(glDraw*调用),纹理对象将被填充,你可以为任何你想要的东西使用它。这里有个例子:
// Drawing to Off-Screen Surface
.
// Create and bind the Frame Buffer.
// Create and attach the Render Buffers, except the render buffer which will
// receive the texture as attachment.
GLuint _texture;
glGenTextures(1, &_texture);
glBindTexture(GL_TEXTURE_2D, _texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, textureWidth, textureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
.
为一个像素数据创建一个纹理不同,此时你设置纹理数据为NULL。因为他们稍后会被动态的填充。如果你想要输出图片纹理作为另一个绘制,记住第一个绘制的对象会填入纹理。
好吧,任何帧缓冲操作,最好检查glCheckFramebufferStatus以查看是否已附加所有内容。有一个新的问题:如果想要保存解析的纹理到一个文件,怎样从纹理数据获取像素数据?OpenGL想的很周到,给我们提供如下函数:
// Getting Pixel Data from Texture
GLvoid glReadPixels(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid *pixels)
* x: 从X位置开始获取像素数据。记住在OpenGL中像素的顺序,从左下角开始到右上角结束
* y: 从Y位置开始获取像素数据。记住在OpenGL中像素的顺序,从左下角开始到右上角结束
* width: 获取像素数据的宽度。不能比原始渲染缓冲区大
* height: 获取像素数据的高度。不能比原始渲染缓冲区大
* format: 总是GL_RGB,还有其它格式,但它取决于实现,并且可能会因供应商而异。例如:获取alpha信息,要根据EGL上下文配置,取决于供应商。
* type: 总是GL_UNSIGNED_BYTE,还有其它格式,但它取决于实现,并且因供应商而异
* pixels: 指向返回像素数据的指针
正如你所见,此函数非常简单,你可以在想要的时候调用。只需记住一件重要的事:OpenGL像素顺序!它是从左下角到右上角。对于一张传统的图片文件,那意味着图片被垂直翻转,因此当你想要保存为一个文件,要注意这点。
现在,你必须执行导入纹理时惯用的反转路径。你有了纹理数据,想构建一个文件。幸运的是很多语言提供了一个简单的方式从像素数据构建一张图片。如:在Objective-C的Cocoa Touch,可以用NSData + UIImage,用如下方式:
// Drawing to Off-screen Surface
.
// The pixelData variable is a "void * " initialized with memory allocated
glReadPixels(0, 0, 256, 256, GL_RGB, GL_UNSIGNED_BYTE, pixelData);
UIImage *image = [UIImage imageWithData: [NSData dataWithBytes: pixelData length: 256 *256]];
// Now you can save the image as JPG or PNG
[UIImageJPRGRepresentation(image, 100) writeToFile: @"A path to save the file" atomically: YES];
.
一个小问题:glReadPixels从哪里读取?OpenGL状态机,还记得么?glReadPixels将从最后帧缓冲区边界读取像素。接下来讨论下优化。
提示和技巧
现在我想谈谈增强你的应用程序的一些技巧。我不想谈论一些使你获得0.001秒的优化。我想谈谈真正的优化。那些可以提高0.5秒,甚至可以提高渲染帧速率。
缓存
缓存非常重要,我非常爱它,我在所有的东西上常使用它,非常棒!想像下这种情况,用户在屏幕上触摸一个物体旋转。现在用户触摸另一个物体,但第一个没有改变任何东西。那让第一个物体的变换矩阵作一个缓存矩阵将是非常好的,而不是每一帧计算第一个对象。缓存概念可以延伸到其它地方,像相机,光照和四元数。为了不在每一帧重计算某些对象,使用一个布尔数据类型去检查一个矩阵或一个值是否被缓存。下面的伪代码显示了如何轻松使用缓存概念。
// Cache Concept
.
bool _matrixCached;
float _changeValue;
float *_matrix;
float *matrix(void)
{
if (!_matrixCached)
{
// Do changes into _matrix
_matrixCached = TRUE;
}
return _matrix;
}
void setChange(float value)
{
// Change the _changeValue which will affect the matrix
_matrixCached = FALSE;
}
存储值
我们常在变换改变矩阵或四元数。如,我们的代码进行了更改:转换X-我们更改最终矩阵,转换Y-我们更改最终矩阵,旋转Z,绽放Y,我们都更改最终矩阵。一些3D引擎和人们甚至不会对这些变换值持有,因此,如果代码要重新获取这些值,他们直接从最终矩阵获取这些值。但这不是最好的方法。如果我们独立存储这些值,像移动的X、Y、Z,旋转的X、Y、Z和缩放的X、Y、Z将会达到一个很好的优化。通过存储,你可以对结果矩阵进行一次更改,每帧进行一次计算,而不必在每次转换时进行计算。下面的伪代码将更好的帮助我们理解存储概念:
// Store Concept
.
float _x;
float _y;
float _z;
float x(void) { return _x; }
float setX(float value)
{
_x = value;
}
float y(void) { return _y; }
float setY(float value)
{
_y = value;
}
float z(void) { return _z; }
float setZ(float value)
{
_z = value;
}
float *matrix(void)
{
// This function will be called once per frame.
// Make the changes to the matrix based on _x, _y and _z.
}
C总是最快的语言
这个技巧是你要记住的。你可能知道,但要加强。C是最快的语言。没有什么语言比C更快。它是最基础的语言,是所有计算机语言最快的。因此总是要尝试在代码的最关键的部分使用C。尤其是渲染例程。字符串比较使用C是使用Objective-C将近4倍速度。因此如果你在渲染的时候要检查字符串的值,最好将NSString转换为C字符串(char *),然后做比较,即使你还需要将C字符串转回NSString,在这种情况下C字符串也更快。比较C字符串,只要使用
if(strcmp(string1, string2) == 0)
。尤其对于数字,总是使用C基本数据类型(float,int,short,char及它们的unsigned类型)。此外,避免最大值,使用64位,如long或double数据类型。记住OpenGL ES默认不支持64位数据类型。
总结
好的,伙计们,我们在此系列的终点。我敢肯定,现在你了解有关OpenGL和3D世界的很多知识。在该系列的3个教程中,我几乎涵盖了有关OpenGL的所有内容。我希望你了解这些教程中涉及的主题的所有概念。现在,随着我们的使用,让我们记住一切:使用OpenGL的2D图形可以通过两种方式完成:1.有或没有深度渲染缓冲区。2.当使用深度渲染缓冲区,最好使用具有正交的投影相机。3.与你选择的方式无关,始终将网格概念与2D图形一起使用。4.多重采样过滤器是一个取决于供应商实现的插件。多采样总是会在性能上付出很大的代价,仅在特殊情况下才可以使用它。5.始终尝试将纹理优化为2bpp数据格式。有时可以在应用中使用PVRTC去保存从文件中创建的OpenGL纹理。6.在使用矩阵时始终尝试使用缓存概念。7.利用“存储值”概念来节省CPU处理。8.在关键的渲染例程上首选C语言。
好吧,现在呢?就这些?不,这将永远不够!点和线值得特别介绍。带有点可以制作粒子和很酷的效果。正如我在本教程开始时据说的,在没有深度渲染缓冲区的情况下,可以使用带有2D图形的点而不是正方形。关于着色器?更深层?可编程管线为我们提供了一个全新的编程世界。我们应该讨论曲面法线&顶点法线,切线空间,法线凸凹效果,反射和折射效果。说实话,我认为我们需要一个名为“All about OpenGL Shaders”的新系列教程。好吧,这可能是我的下一个系列。