Opengl在游戏设计中的运用(part-1)

翻译人员:蓝羽,糖炒小虾,sharyu, benna,jesse

校对:子龙,蓝羽,yuezang,Iven

 

开篇:最近观察发现在cocos2d-x和cocos2d-iPhone开发中,很多同学都会遇到这样那样的开发瓶颈,归根结底,大多还是对cocos2d的本源-OpneGL知识的欠缺,为此,泰然翻译组再次推出OpenGL系列,让大家更详细的了解OpenGL。(byIven



OpenGL 2D图形

OpenGL绘制2D图形不仅仅限于线图元和点图元的的绘制。三图元(三角形,线,点)在2D,3D下都很好用。2D图形首先要知道Z 的深度。在2D图形中,除了z变换,z缩放(远景、近景)以及x和y的旋转之外,我们所有的工作变成二维的了。这意味着我们不再需要深度渲染缓存了,因为我们是在相同的Z 位置上来画所有东西的(通常是0.0)。



一个问题随之而来,“OpenGL怎么知道一个物体是画在其它物体的前面?”这非常简单,按顺序绘制物体(在后面的物体先绘)。OpenGL提供一个特性叫做多边形偏移(Polygon Offset),但是这个特性更像是调整而不是真的排序。
好了,我们可以用三种方式思考2D图形:
相同Z位置的一些正方形。

1.相同Z位置的一些点。

2.以上所有

clip_image002

对OpenGl来说, 2D图形看起来的样子

clip_image004

2D场景是怎样显示到屏幕上的

你可以想象这对OpenGL来说多么容易,一个状态机准备同很多三角形一起工作,处理一些三角形。在极端的情况下,2D图形要使用成百的三角形来工作。

简单来说,一切都是纹理。所以我们大部分2D工作都在纹理上,一些人很想创建API来使用Non-POT[POT: Power Of Two,表示图片的长宽像素是2的n次方]纹理,这就意味着要使用一些像36x18,51x39等等这样尺寸的纹理。我的建议是:"不要这么做"。2D图像使用Non-POT纹理不是好主意,正如你从上面图片看到的,使用虚构的网格总是好的,这个网格应该是POT,16x16或者32x32可能是一个好选择。

如果你计划用PVRTC压缩图像文件,最好是用8×8的网格,因为PVRTC最小尺寸是8。我不建议制作小于8×8的网格,因为这没必要那么精确,会增加你的工作负担,也会影响你程序的表现,8×8网格已经非常精确,下面我们会看到不同网格的区别,以及什么时候和怎么使用它们。让我们更多的讨论网格。


网格(Grid)概念

我认为这是2d应用制作中最重要的一部分。

在3D游戏中,例如,去判断一个角色能移动到哪里我们需要创建一个碰撞检测器。

这个检测器可以是个框(边界框 bounding box) 或者是个网(边界网 bounding mesh,只是原来的简单复制)这两种情况,计算是非常重要且昂贵的。但是在2D应用中,如果你使用网格(grid)去寻找碰撞区域会变得非常非常容易,因为你只需要获得一个用XY坐标表示的矩形区域。

这也恰好是网格非常重要的原因之一。我知道你可以举出很多网格的特性例如: 它们是有组织结构的的(网状的),高精确的,能精确地定位屏幕上的物体等等。大约十年前(或者更早) 我写了一个很简单的2D RPG游戏,在那个游戏里面就已经使用网格的思想了。

下图展示了 游戏里面的所有东西是怎么放到网格中的。

clip_image005

RPG Maker效果图

我知道,我知道。。。这是”fuckwindows” 系统,对不起。 正如我所说,这是十年前的东西。好了,还是让我们来了解一些关于网格的重要特性吧。你可以点击左侧的图来放大它。我想让你注意的第一件事是关于图片的重叠部分。注意窗口左侧的这些小图标,它们可以看作是某种工具库。 在库最顶端,你可以看到一些小方形图(在这里是32 * 32的尺寸)。这些方形图用来组成游戏的场景(在我们的OpenGL语言中,他应该是背景(background))。库中的其他图是带有透明像素的png图,用于放到地板上面的。你可以通过观察grid上放置的大树们发现和地板的区别。

现在去寻找网格上的“英雄”。他在窗口右侧的一棵树的旁边,他红头发,有一张小脸,且被方框圈上。这是gird第二个重要的地方。英雄不只能占据grid上的一个方格,他可以变得更大。但是在grid上,动作分界符只表现在一个方格中。困惑了?好吧,只使用gird上的一个方格来处理动作一直是一个好方法,因为他让你的代码比其他方法更有条理。你可以通过复制动作方格来创建action区域,例如你可以通过右侧顶部区域的方格离开村子。

我确定你可以很容易地在你的2d应用中创建一个控制器类来处理actions,然后创建一个视图类与控制器类相关联,然后你就可以通过视图类在grid的多个方格上获得碰撞检测信息了。

所以假设现在有一个action,它是一个N个gird方格区域的检测器。通过这个方法,你可以充分利用gird的特性,并且提高你的应用程序的性能。

通过使用grid你可以轻松定义那些人物不能穿过的碰撞区域,例如墙壁。另外一个特性是使用grid定义“顶层区域”,他的意思是,这些区域一直在背景上面绘制,例如高层的树们。所以如果游戏主角穿过这些区域,他就会被树挡住。

下图 展示了一个 集成gird所有特性于一身的最终场景。你会发现有好多图片都相互重叠了。注意角色是怎么处理行为区域以及它的顶部区域。然后注意最顶层的效果覆盖了下层的所有东西,像云的阴影或者太阳射出的黄色的光。

clip_image007

RPG Maker 最终效果

总结提到的知识点,gird的确是制作2D应用非常重要的一部分。Gird在OpenGL中不是具体存在的事务,所以你需要小心的使用它的特性,因为任何事情都是虚构的。好吧,你还需要知道,一个附加的信息:gird概念是多么重要,OpenGL内部都用grid的概念来组织片断(Fragments)

好了,这是所有关于grid的东西。现在你可以说:“OK,但是这些不是我想要的,我想要一个像暗黑破坏神(Diablo)、罪恶之城(Sin City)、We Rule一样设计的游戏”。好吧,让我们去研究一些更复杂的事情并且给我们的2D应用带来Depth Render Buffer(深度渲染缓缓存)和Cameras(镜头)。(详情可参见泰然教程组作品:从零开始学习OpenGL ES之三 – 透视



2D中的深度渲染缓冲

知晓2D图像是如何与OpenGL结合在一起的,我们可以用更精细的方法去思考,比如在2D应用中使用深度缓冲。

clip_image009

"2D 游戏"使用了带有深度渲染缓冲的OpenGL(点击放大)

clip_image011

"2D 游戏"使用了未带有深度渲染缓冲的OpenGL(点击放大)

点击上面的图片后,你会发现它们的不同之处。两张截图都来自iOS上的著名游戏,都使用了OpenGL,并且都是2D的。尽管它们使用的是OpenGL ES 1.1,我们仍然可以理解到网格和深度渲染缓冲的概念。左边的游戏(枪火兄弟连,Gun Bros)使用了非常小的网格,实际上只有8*8像素,这种网格使得游戏可以用难以置信的精确度来放置物体,但是要改进用户体验的话,你需要生成一系列方格来处理动作,在这个例子中,一个好的选择是每个动作检测器安排4到8个方格。右边的游戏,叫做Inotia(艾迪诺亚),顺便说一下,Inotia目前已经出到第三代了。自从第一代开始,Inotia一直使用较大的网格,32*32像素。和Gun Bros一样,Inotia也使用了OpenGL ES 1.1。

这两种网格(8*8和32*32)之间有很多不同之处。第一种(8*8)更加精确,看起来是最好的方案,但记住它会增加许多运算。Inotia游戏只有很少的运算需求,对iOS的硬件来说可以忽略。你需要为你计划使用的应用,选择最佳的方案来适应其需求。

现在,来谈谈深度渲染缓冲,其中最伟大的事情莫过于你可以在应用中使用3D模型。瞧瞧,如果没有深度渲染缓冲,你只能使用带纹理的方格,或其它的原始几何形状。这样做以后,你必须在你的动画中的各个位置放置不同的纹理,特别是角色动画,显然,使用纹理图集是个不错的办法。

clip_image013

来自Ragnarok的角色纹理

Inotia游戏中各个角色都有着和上图类似的纹理图集。图片中,你可以看到屏幕上的三个角色只能在四个方向上转向。现在,来看看上面Gun Bros的图片。

注意这些角色可以转向所有方向。为什么?使用深度渲染缓冲可以自由地在2D应用中使用3D模型。所以你可以根据网格和2D原画旋转,缩放和变换3D模型(Z轴不变换)。结果会更好,但和所有改进一样,这种方法当然比2D方格消耗更多性能。

但是,关于混合3D和2D原画还有一个重要的事情:摄像机的使用。除了在屏幕的正前方创建一个单独的平面,取而代之,你可以锁定Z轴变换,创建一个沿Z轴的平面,如同3D应用中将物体放置于平面上,然后创建一个正投影视角的摄像机。你想起这个方法并且知道如何实现了不是吗?

在深入探讨2D图像的摄像机和深度渲染缓冲之前,重要的是知晓2D和3D图像对于编码层面来说,没有实际的区别,因为所有事情都取决于你的计划和组织。

现在,让我们谈论2D图像的摄像机。



The Cameras with 2D 2D摄像机

好了,我相信你知道如何创建一部摄像机和正交投影。现在就像你看到的,这篇教程是关于摄像机的,是么?。那么就出现了一个问题:“什么地方是使用摄像机和深度渲染2D图像缓存的最佳位置?”下例的图像可以胜过千言:

clip_image014

正交投影和透视投影下相同的摄像机机位对比图像(点击放大)

这张图像显示了一个类似Diablo游戏风格的场景,图中摄像机在相似的机位。你会明显地注意到两种投影的差别。注意横穿图像的几条红线,在正交投影中这些线是平行的,但在透视投影中这些线并不真的平行,在无穷远处会相交。

现在关注图像右下角的灰色缩略图。这是一个存在着一些物体的场景。就像你看到的,它们是真实的3D物体,但在正交投影下,你能将2D外观应用于你的3D应用程序,创建像Diablo, Sim City, Starcraft或其他畅销游戏的场景。

clip_image015

如果你再看一下Gun Bros游戏的图像,你就能理解这就是他们所做的,在场景中放置真实的3D物体,并使用正交投影的摄像机。

在你想要的位置创建摄像机的最佳方式是将你的所有场景建设在一个3D世界,设置摄像机为正交投影,使用网格的概念来引导你的空间变化。

clip_image016

网格概念甚至和摄像机、深度缓存渲染一样重要。

我有一个关于这个主题的新建议,好吧,这不是一个真的建议,这更像是一个警告。透视投影和正交投影是完全不同的。所以你会发现相同的配置,如视点、视角、近裁剪平面和远裁剪平面,将产生完全不同的结果。

好了,这是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来实现,它需要一个由厂商负责的插件。每个厂商必须制作一个EGL插件来引导相关的必要信息。只有这样OpenGL才能真正地解决多重采样。默认的EGL API提供了多重采样配置,但通常厂商会做一些改变。

在苹果公司的例子中,这个插件叫做“Multisample APPLE”,它放在OpenGL扩展头文件(glext.h)中。正确实现苹果公司的多重采样,需要用到2个帧缓存和4个渲染缓存!一个帧缓存是由OpenGL提供的。另一个是多重采样的帧缓存。渲染缓存是颜色和深度的。

使用苹果公司的多重采样,在glext.h中有三个新的功能

MULTISAMPLE APPLE
GLvoid glRenderbufferStorageMultisampleAPPLE(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height)target: 这个target总是 GL_RENDERBUFFER, 这是OpenGL的内部惯例

samples: 这是多重采样滤镜样本制作数量

internalformat: 它指明了我们想要什么类型的渲染缓存,以及临时图片使用什么色彩格式

     GL_RGBA4, GL_RGB5_A1, GL_RGB56, GL_RGB8_OESGL_RGBA8_OES颜色缓存

     GL_DEPTH_COMPONENT16GL_DEPTH_COMPONENT24_OES深度缓存

width:渲染缓存最终宽度

height: 渲染缓存最终高度
GLvoid glResolveMultisampleFramebufferAPPLE(void)·这个方法不需要任何参数。这个方法仅仅分解最后2个帧缓存,分别绑定到GL_DRAW_FRAMEBUFFER_APPLE和GL_READ_FRAMEBUFFER_APPLE
GLvoid glDiscardFramebufferEXT(GLenum target, GLsizei numAttachments, const GLenum *attachments)target: 通常target为 GL_READ_FRAMEBUFFER_APPLEnumAttachments:  在目标帧缓存中要清除的渲染缓存attachments的数量,通常是2,清除色彩和深度渲染缓存。

attachments:{GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT}.一个指向一个数组的指针,包含要清除的渲染缓存类型。通常数组是{GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT}

在我们看代码之前,让我们来稍微理解一下这些新方法。第一个方法(glRenderbufferStorageMultisampleAPPLE)是用来替换glRenderbufferStorage方法的设置渲染缓存的属性功能。这个方法中最大的改变是样本数量,它可以设置每个像素的样本数量。

第二个(glResolveMultisampleFramebufferAPPLE)是用来从原有的帧缓存中获取信息,然后把它们放到多重采样帧缓存中,解析这些样本,然后再把结果图片放到原来的帧缓存中。简单地说,这是Multisample APPLE的核心,在这个方法中做了所有工作。

最后一个(glDiscardFramebufferEXT)是另外一个清除的方法。就像你想象的那样,当glResolveMultisampleFramebufferAPPLE做完了所有的处理后,多重采样帧缓存中会有很多信息,那么是时候去清理掉所有的信息了。那么,我们通过调用glDiscardFramebufferEXT来通知它需要清理的地方。

现在,这里有使用Multisample APPLE的所有代码:

MULTISAMPLE FRAMEBUFFER 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 height 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, &_msaaDepthBuffer);

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

//————————-

// Clears normal Frame Buffer

glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Clears 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的信息,点击关于EGL和EAGL的文章(未翻译).OpenGL也提供了一些多重采样,glSampleCoverage的配置和一些glEnable的配置。我在这里不深入讨论这些配置了,因为我不认为多重采样是一个值得我们花时间的东西。就像我跟你说的,结果没有什么大不了的,只是稍微有点好看。在我的观念中,对于得到的结果来说,它花费了过多的性能。

clip_image017

相同3D渲染模型不带和带反锯齿滤镜

好了,是时候讨论OpenGL中的贴图了。下一篇教程将重点介绍Opengl中的贴图。

标签: 音频教程

?>