---页首---

翻译:关于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我们的想象不会受到约束,我们可以自由翱翔。

让我们开始吧

这里有一个小目录,方便你阅读:

前言

记住下面的每点

  1. OpenGL的逻辑由3个简单的概念构成:原语、缓冲区和栅格化
  2. OpenGL 2.x工作原理是可编程管线,也就是着色器的同义词
  3. OpenGL不关心输出的设备,平台或者显示的屏幕。为了桥接OpenGL与我们的设备,我们必须使用EGL(或iOS中的EAGL)
  4. 纹理是非常重要的,要指定一个像素格式和顺序,以适应OpenGL
  5. 我们通过调用glDraw*开始渲染过程。首先将通过顶点着色器,一些检查将得出结论,即处理后的顶点是否可以进入片段着色器
  6. 我们网格的原始结构永远都不会改变。我们只是创建转换矩阵以产生所需的结果

首先我们将讨论2D图形,然后让我们看看什么是多重采样/抗锯齿滤波器,我个人不喜欢这种技术带来的未知收益。很多情况下,无需多重采样可以很好的运行应用程序,但是简单的多重采样过滤器就可能会完全破坏该应用程序的性能。总之,有时我们需要临时的多重采样来产生平滑的图像。

稍后我们将深入讨论纹理及其优化,即每个像素2字节的数据格式。同时也看看PVRTC和怎样放入OpenGL纹理,以及渲染到屏幕外的表面。

最后,我将简要介绍一下我自己发现的一些性能收获。一些提示和技巧对今天的我仍有很大的帮助,也与你们分享。让我们开始吧!

OpenGL 2D图形

将2D图形与OpenGL配合使用并不一定限于使用线或点图元。三个基本原语(三角形、线和点)很好的被用于2D和3D。关于2D图形首要的是Z深度。我们所有的工作将在2维上,不包括Z轴平移和绽放,也不包括XY轴的旋转。那表明我们不需要使用深度渲染缓冲区,因为我们所有的绘制将会在同一个Z的位置(通常是0.0)。随之而来的一个问题:OpenGL怎么知道哪个将被绘制在另一个的前面(或者上面)?非常简单,通过我们想要绘制图像的顺序,在后面的要被先绘制。OpenGL也提供了一个功能,被称做:多边形偏移量,但它更像是一种调整,而不是实际的排序。现在我们从三个方面思考2D图形:

  1. 在相同的Z位置有很多正方形
  2. 在相同的Z位置有很多点
  3. 以上两者均是

2d_orthographic_example
2d_scene_example

你可以想象对于有百万个三角形状态机的OpenGL处理几个三角形是多么容易的一件事。在极端情况下,2D图形工作在几百个三角形。简而言之,所有都是纹理。因此大部分2D工作将是在纹理上。许多人感觉被迫创建API可以与非2的次幂(Non-POT)纹理一起使用,那就是说要使用3618、5139等尺寸的纹理。我的建议是:不要那样做!工作在不是2的次幂的纹理不是一个好的想法。在上面的图片中,使用虚构的网格(应该是POT)总是一个好的主意,使用1616,3232将是一个好的选择。如果你计划用PVRTC压缩图片文件,应该使用一个88的格子,因为PVRTC最小的尺寸是88。不建议使用比88小的尺寸,没有必要提高精度,也会增加开发工作,还会损耗应用的性能。88的格子精度已足够,我们将会很快看到网格之间的差异以及何时、如何使用它们。让我们再多谈一些网格的知识吧。

网格概念

我认为这是2D程序规划中最重要的一部分。例如,在3D游戏中,决定一个角色能否走动必须创建一个碰撞检测器。检测器可以是一个盒子(边界盒子)或者网格(边界网格,是原始网格的简单副本)。在两种情况下,计算是非常重要且昂贵的。但在2D应用中如果你使用网格找碰撞区域是非常容易的,因为你只使用一个正方形区域XY坐标。这仅只是一个原因,因为网格非常重要。我知道你可以提出网格的许多其它的优点,例如组织,计算精度,屏幕上对象的精度等。在十年前或更久,我制作了一个小程序,用2D图形生成RPG游戏。网格的想法在那里就已经很成熟了。下面的图片展示一切是怎样放入网格中的:

rpg_maker_example1

让我们关于网格的重要特性。你可以点击旁边的图像放大它。首先想你注意到覆盖在图片上东西。注意到左边窗口是保留的一个库。在库的顶部可以看见一个32*32的小正方形。这些正方形保留在场景的底部(在我们的OpenGL语言中,将是背景)。库中的其它图片都是透明图片(PNG),可以被放在底部正方形的上面。通过查看网格上的大树,你可以看到这种差异。在格子上寻找“英雄”。他在靠近一颗树的右边,在一个盒子中带着红色的帽子,脸较小。这是关于网格的第二个要点。那个小人不只占有一个正方形网格,可以更大,但对于网格来说,动作分割符仅代表一个正方形。不懂?通常仅用一个网格的正方形处理动作是一个好主意,因为这样让你的代码组织起来比其它方法要好很多。创建动作区域,你可以复制动作的正方形,就像图片网格中右上角的区域的村庄出口。我确定在你的2D应用中创建一个控制类处理动作,然后创建一个视图类引用控制类是一件很容易的事,那么你可以准备视图类在网格的正方形获取碰撞检测。因此,你有1个动作-N网格方格检测器。这样你可以利用网格的所有优势,并可以极大地提高应用程序的性能。通过使用网格可以轻松定义角色无法穿过的碰撞区域,例如墙壁。另一个使用网格的优势是定义“顶部区域”,也就是,总是绘制在最上面的区域,如上面的树。如果角色穿过这些区域,将展示在后面。下面的图片是使用网格所有的概念展示的最终场景。注意有多少可以覆盖在图片上面,注意角色如何处理动作方块及顶部区域。同时要注意重叠所有内容最上面的效果,像云朵的阴影或太阳的光照。

rpg_maker_example2

总结要点就是:网格是规划2D应用的最重要的部分。网格在OpenGL并不是真实的事物,所以你要特别小心使用这个概念,所有都可以被想象。好吧,为了让你知道更多的信息:网格概念非重要,以至于OpenGL在内部与网格概念协同工作来构造片段。非常好,这是关于网格的所有。现在你会说:好的,但这不是我想要的,我想要一款像《暗黑破坏神》、《罪恶之城》甚至《We Rule》那样的投影游戏。哦,对了,让我们把事情变得更复杂,然后将深度渲染缓冲区和摄像机带回到2D应用中。

2D深度渲染缓冲区

了解2D图形如何与OpenGL配合使用,我们可以考虑采用更精细的方法,甚至像在2D应用中使用深度缓冲区。

grid_depth_example

grid_normal_example

点击上面的图片,你可注意到他们的不同处。都是来自著名的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模型。看起来,没有深度渲染缓冲区,你只能使用带有纹理的正方形或其它原始几何形式。通过这种方式,你只能为每个位置动画信息创建不同的纹理,尤其是角色动画,明显一个更棒的主意是利用纹理图集:

texture_atlas_example

上面Inotia游戏的图片在游戏中出现的角色有相似的纹理图集。看上面的图你可以发现在屏幕上的三个角色只能向四个方向。现在再看一下上面的Gun Bros的图像。注意到角色可以你任意方向转动。为什么?很好,通过使用深度渲染缓冲区可以在2D应用中自由的使用3D模型。因此你可以根据网格和2D概念旋转、缩放和移动3D模型(无Z轴平移)。结果要好得多,但是如果进行任何改进,则与2D正方形相比会产生很大的性能成本。但有另一个重要的事情关于混合3D与2D的概念:相机的使用。你可以在Z轴上创建一个大平面,像在3D应用程序中一样放置对象,并创建具有正交投影的相机,而不是在屏幕前面立即创建一个平面,无需锁定Z轴平移。你还记得和如何去做么?点击这里,关于相机。在2D图形深入讨论相机和深度缓冲区前,知道这一点并没有真正的区别是非常重要的,在代码层面,2D和3D图形所有的东西都来自你自己的计划和组织。因此使用深度缓冲的代码与第二篇教程一样。现在让我们讨论2D图形使用相机。

2D相机

如你已经看过这篇关于相机的教程(camera tutorial)现在我确定你知道怎样去创建相机和正交矩阵。那现在有一个问题:在2D图形中使用相机和深度渲染缓冲最佳的地方和方法是哪里?下面的图片说明胜过千言万语:

cameras_projection_example

此图展示了一个类似Diablo游戏的场景,使用相机在相似的位置。你可以在两个矩阵清晰的看到不同之处。注意图片上的红线,在正交投影中,这些线是平行的,但是在透视投影中,这些线不是真正的平行,可以在无穷远处相交。现在让我们集中注意边在左下角灰色绽放的图片上,那是有物体的场景。正如你所见,那是真的3D物体,但在正交矩阵下你可以创建场景如Diablo、Sim City、Starcraft、其它畅销书之类的场景,为你的3D应用程序提供2D外观。如果你用另一个视角看Gun Bros游戏的图片,你会发现看到的正是他们所做的,其中有一个带正交投影的相机和放置在场景中的真实3D对象。在你期望的位置创建相机的方法是在3D世界构建你所有的场景,用正交矩阵设置相机,同时通过使用网格引导你的空间。

cameras_grid_example

关于这个主题我还有最后一个建议,确切说是一个警告。透视和正交矩阵是完全不一样的。因此相同的焦点、视角、近端和远端相同配置会产生完全不同的结果。因此,你需要为本要使用正交投影配置,而不是使用透视投影配置。可能你使用透视矩阵正常显示,使用正交矩阵你可能看不见任何东西。这不是错误,这是透视与正交计算间和不同之处。关于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的纹理。

multisample_result_example

更多关于纹理

从此系列的第二部分我们已经了解很多的关于纹理知识。首先,我们讨论优化的类型。对我们应用的性能有很大的提高和非常容易实现。我们即将讨论图像每像素的字节数。

每像素字节数

通常,图像每像素有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_4GL_UNSIGNED_SHORT_5_5_5_1GL_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通道用前面一个,不需要就用后面一个。关于最后一个类型的一点小注意。人类的眼睛对绿色更敏感,所以绿色通道有更多的位数。用这种方式,即使更少的颜色范围,解析的最终图片看起来也不会太不同。好,让我们看下两和压缩间的不同。

texture_compression_example

如你所见,使用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_4GL_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_4GL_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”的新系列教程。好吧,这可能是我的下一个系列。

posted @ 2021-04-26 22:09  20190311  阅读(241)  评论(0编辑  收藏  举报
---页脚---