状态管理(State Management)

翻译:丛越

本文只为学习之用,版权所属原作者,如有转载,请注明出处。

原文地址http://www.delphi3d.net/articles/viewarticle.php?article=stateman.htm

 

状态变化是昂贵的,并且应该避免。在这篇文件里,我将大体描绘出一个可以帮助最小状态变化的渲染架构。我将使用 OpenGL 术语,当然,原理上可以应用于任何图形 API

 

我先从定义几个贯穿文章中的术语开始。然后我将会谈论我们如何描述状态变化——毕竟假如我们不能描述他们,我们就不能管理他们。接下来,我将讨论我们怎样能建立一个基于渲染状态的批量多边形渲染器,这样可以避免冗余的状态设置。最后,我将提供几个额外的优化技巧。

术语

你已经知道状态变化(State Change)的定义了,或者至少我希望这样。我们将任何影响Fragment 渲染的 OpenGL函数调用定义为一个状态变化。这其中包括纹理、材质、光照或者混合变化。纹理时目前最昂贵的状态变化,所以普通的优化是用纹理对多边形排序。这意味着你不必调用多余的 glBindTexture(),这样可以极大地提高性能。

 

对你的应用程序来说只是避免纹理变化也许还不够,如果你大量的使用 OpenGL 的光照,你会希望更避免多余的材质变化。我们将会使用更通用的方法来寻找一个能避免所有多余的状态改变的系统。

 

我们假设场景由几个对象组成,或者网格(meshes)。网格只是多边形的几何。这些多边形在逻辑上不一定聚在一起。他们的共同之处仅仅是他们有着把他们关联在一起的相同渲染状态设置的集合。我们称这样的状态设置集合为 shader —— 也许这种定义让每个人想起了 Quake 3 Arena 的术语。一个shader 可以包含下列信息:

 

两个纹理,以及如何将他们混合在一起的信息。我们假设我们希望使用笨重的多纹理(multitexturing)。
材质参数。这包括 ambient, diffuse, specular emissive 颜色,还有shininess .
混合函数。

 

当然,我们可以在 shader 里加入任何我们想要的东西。实际上这依赖于我们要改变哪一个状态设置。例如如果我们希望支持环境贴图,我们必须在 shader 中加入必要的纹理生成 (texgen )参数。

 

实际上为避免状态改变, 我们需要根据meshshader进行排序。为了找到一个好的shader排序方法,我们会将shader插入到shader tree中。树的顶部描述最昂贵的状态变化,反之叶子则描述最便宜的状态变化,让我们通过更多的细节来观察它是如何运作的。

• Shaders

我们将插入一个状态变化到树中,所以一个 Shader 在遍历这个数的时候会被重构。我们可以说我们有如下Shaders,每个Shader由两个纹理,一个材质和一个混合模式组成。:

Shader 1
• BrickTexture
• DetailTexture1
• RedMaterial
• OpaqueBlending

Shader 2
• BrickTexture
• DetailTexture1
• GrayMaterial
• OpaqueBlending

Shader 3
• BrickTexture
• DetailTexture2
• RedMaterial
• OpaqueBlending

Shader 4
• FXTexture
• No texture
• ChromeMaterial
• AdditiveBlending

这就形成了这样的 shader tree:

 http://www.delphi3d.net/articles/shadertree.gif

注意每个树中的节点可以拥有任意数量的孩子。思路是将最昂贵的状态变化放置在层次的顶级,所以这种变化会很少发生。在这个实例中,我们认为纹理变化是最应该避免的。

可是这意味着一些状态将不得不重复设置。在这个例子中,我们通知系统切换 Alpha Blending 状态3 次。我们接受这个事实,因为移动混合模式到树的更高级别会增加一定数量(更昂贵的)的材质变化。我们稍后会看到这实际上不是什么问题,因为我们仍然能避免多余的状态变化。

首先,让我们来看另一个shader 。最后的shader只有一个纹理贴图,因此它没有使用多纹理贴图。为了让我们的系统运转,这个信息(例如:没有第二个纹理)也必须被记录在shader tree中。一般来讲,如果一个 shader 缺少任何所支持的状态值,我们将给它设置一个缺省值。

如果我们有适当的文件格式和我们头脑中能清晰地描述状态变化,建立shader tree是一个简单的过程。我建议创建一个抽象基类来描述状态变化(任何状态变化),并且派生类来描述纹理、材质或者混合函数变化(或者其他的应用的需求)。

渲染

既然我们有了一个shader tree,我们就可以用最少数量的状态变化来渲染场景。我们通过一个分支一个分支的遍历shader tree来做到这一点。在每个节点,我们设置存储在节点上的状态。树中的每个叶子关联到一个shader,因此当我们到达叶子时,就可以用这个shader渲染所有的mesh

在介绍中,我规定每个mesh都关联一个shader,但是这里我们需要颠倒这个关系。我们应该建立一个所有shader的列表,每个shader存储一个使用到它的mesh的列表。作为一个与shader tree一起构造的预处理步骤, 这点极易实现。

这个渲染方案意味着所有的mesh一定是通过一个通用的接口来进行渲染。最简单的方法是为可渲染(renderable)的对象创建一个抽象基类。我们可以派生子类使用 glBegin()/glEnd()绘制多边形,通过渲染一个顶点数组,或者通过调用一个显示列表。

进一步的改进

我们仍然有几个可以做的事情来改进状态管理。对于初学者,还记得我们是如何被迫多次插入某个状态变化到shader tree中吗?在我们的例子中,3个连续的shader共享同样的混合设置。这导致多余的状态改变,但是这很容易避免。我们能做的是维护一个当前状态的列表。这个列表包含了指向当前在 effect 中状态设置的指针。在shader tree中的每个节点,我们检查当前的状态,看我们是否重复设置了这个状态改变。如果是,忽略,否则将用这个变化来更新当前的状态。

我们也实现一个通用的渲染mesh的接口。从我们的角度来说,这是很优雅并且灵活的,因为这不会将我们束缚在单一描述mesh的内存上。但是在 OpenGL的角度来说,这个方法有点。为了更大发挥3D显卡的功效,我们应该通过最优化的驱动路径渲染所有的mesh。对于大多数驱动,应该使用 glDrawElements合并顶点数组。

如果我们能用顶点索引数组描述所有的mesh,状态管理能带来额外的一些优化。例如,在 GeForce 显卡上,状态管理在AGP内存上分配一个顶点缓冲区并且把mesh放在其中。这意味着我们可以大批量的渲染多边形,这是很好的事情。这也能改善了CPU3D显卡的并行性能。如果频繁的在mesh中复用顶点,状态管理也能在mesh开始渲染之前锁住顶点数组。

结论

开始从状态变化是有害的事实,我们已经差不多开发出了一个能尽量避免变化的机制。这应该在任何一个非一般要求的应用中导致速度提升。我们讨论的系统是很普遍的,并且能用于管理任何和所有你的应用依赖的状态设置。此外,这个系统也把实际的几何体传输过程变得简单了,因为所有的顶点在被传输到3D显卡上时必须通过一个单一的接口传输。

然而仍然有几个我们没有涵盖到的问题:

假定有一些透明对象并且需要对他们进行深度排序?
假定需要多遍渲染?
当我们依赖 OpenGL 扩展时,但用户的3D显卡并不支持会发生什么?

所有这些都能被解决,就看我们如何更聪明的组织shader treemesh,所以我希望给读者留下这个练习。如果你想讨论这个话题,不要犹豫给我写信吧!

Advertisements
此条目发表在3D图形分类目录。将固定链接加入收藏夹。

One Response to 状态管理(State Management)

  1. Unknown说道:

    听说最近搜客宝(sokebo.com)比较火,你用过吗?

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  更改 )

Google+ photo

You are commenting using your Google+ account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

w

Connecting to %s