实时阴影绘制技术研究

C++博客 首页 新随笔 联系 聚合 管理
  48 Posts :: 20 Stories :: 57 Comments :: 0 Trackbacks
译者:azure
原文地址:http://www.azure.com.cn/article.asp?id=191

译者序:这是一篇讲解3D引擎中,某些细节优化的文章,文中使用的方法同样适用于其他场合。不过因为本人对这方面的技术和词汇不是很熟悉,所以这篇翻译可能存在很多谬误,希望大家多加批评指正。
原文:Speed-up & Optimization Techniques

绪论
  在这个页面,我收集了一些不同的加速3D引擎的小窍门。我会先介绍一些显而易见的,因为许多人会忽视它们,接着是一些更精彩的。如果你有其他的窍门和算法,尽可以告诉我。

Float --> int 转化
值得尝试的做法,因为这种转化一般会更慢。我知道的这两种方法是用#pragma和内联函数。首先,#pragma:

#pragma aux RoundToInt= "fistp DWORD [eax]" parm nomemory [eax] [8087] modify exact [8087];

这种方法表现的非常好,WDISASM 表明编译器抛弃了对__CHP的调用而内联了这个转化。另一种方法不使用#pragma,可以被移植到其他编译器。这由"InnerSect"提出:

#define FIST_MAGIC ((((65536.0 * 65536.0 * 16)+(65536.0 * 0.5))* 65536.0))
int32 QuickFist(float inval)
{
double dtemp = FIST_MAGIC + inval;
return ((*(int32 *)&dtemp) - 0x80000000);
}

他做了一些测试,说来有趣,如果我没记错的话,他发现上面的方法更快。

提前计算
  提前计算所有数据!你没法提前计算太多东西。不要以为什么都能查表,那样你会失去灵活性。关注不同的方法。首先要提前计算的是你的法向量。表面法向量和顶点法向量。对调试来说非常重要-这些向量的计算耗时惊人。一旦你提前计算了这些数值,你只要像对待对象的其他部分一样旋转他们就可以了。可是不要去平移或者缩放他们。它们必须保持为单位向量。如果你偶然缩放了他们,可以乘以一个相反的比例把他们还原;例如:

| Sx 0 0 0 |
Transform Matrix = | 0 Sy 0 0 |
| 0 0 Sz 0 |
| 0 0 0 0 |
然后你需要这样:
NormalX *= 1 / Sx
NormalY *= 1 / Sy
NormalZ *= 1 / Sz

当你进行裁减和计算光照的时候,你也不需要平移他们。Also you don't need to transform your normals when you perform culling or lighting. 简单的反向平移你考虑到的其他向量,避免矩阵/向量的乘法。你反向平移可以简单的通过重置上方3×3的矩阵来实现:

For every row
For every column
output[row][column] = input[column][row]
End
End

转换灯光,视角向量,以及任何依赖这个矩阵的向量,都因为提前计算而节省了大量工作。你只需要像往常一样计算,用新的向量和旧的法向量。

倒数
  如果你需要做很多次除数相同的除法,取除数的倒数,变成乘法。倒数就是1/n,n是你的除数。如果你确实很聪明,你会发现在运行 fdiv 的时候还有其他事可做。一个不错的窍门是预热高速缓存,高速缓存命中失败会导致从内存中载入数据等占用很多时间。所以,当你坐等fdiv完成的时候,为什么不读一些内存地址呢?那样它们就会被载入高速缓存了。 在我的屏幕渲染的代码里面,这样看起来运行良好。你可以把这个技术用在透视纹理映射上。也有助于你做透视转换。如果想还原那个倒数,再做一次1/n就可以了。非常精彩。如果你喜欢冒险,你可以把数据存储为倒数,例如Z。我还没试过-这会很难调试。

顶点标识
  任意给定一个需要处理的顶点列表,你会发现有很多-也就是说至少一半-是不可见的,在裁减区域之外。你需要快速排除它们。一个简单的方法就是给这些顶点加一个'可见'标志。

  进入处理循环之前,在准备期间,把这个标志设为false。然后,进入循环之后,如果发现顶点是必要的,就设为true。例如,你发现一个三角形可见,就把它所有的顶点可见标志都设为true。在那之后,你可以简单的跳过或者排除那些没有设这个标志的顶点。这对复杂模型非常有用,因为他们包含大量顶点。同样可以这样处理有很多不可见部分的很大的场景,跳过不必要的渲染。如果用于光照,那你必须得小心。如果你进行可见性裁减的时候,没有计算光照,你会发现在边缘的地方出现错误。你在寻找已定义数据和未定义数据的交界,这很费神。。。

  一种方法识用一个计数器来标识。方法很简单。初始化的时候,你把计数器设置为某个非法值,例如-1 (0xFFFFFFFF).。同时,你把帧计数器设置为0。然后在你处理对象的过程中,如果你发现一个面/顶点需要处理,你就把它的计数器设置为和帧计数器相等。反之,不作任何设置和清除。接着,当你实际处理的是时候,你把它和帧计数器对比,那些计数器值和当前帧计数器相等的顶点/面片会被使用,其他的则被跳过。这在大的数据集很方便,因为那种情况下,每帧都正确清除那个标志代价高昂。

指针
  指针非常方便。利用它们你能实现精巧的数据结构,就像链表,二叉树等等。你也可以利用它们快速寻址。比如说你有一个数组,每个元素都是27字节长。你可以把它们填充到32字节,利用移位来计算地址。但是,数组的每个元素都浪费了5个字节。这很浪费。因此,用指针来寻址。就是说你的顶点结构有27字节。在三角形结构中,不要用int vertindex[3],而是用vertex *vertptr[3]。然后简单的载入指针,寻址,就可以了。(译者注:说实话,这段话我没有完全理解,有不对的地方恳请读者指正)

三角形vs 多边形
  三角形易于处理,渲染速度快。但是如果你有一系列6边形构成的表面,你很容易把它们替换为多边形,不过对三角形渲染器来说,你得做6倍的工作。但是三角形渲染更快。个人推荐三角形,在基于三角形的环境操作起来很简单。多边形有它的优势,可能值得一试。如果有人拥有凸多边形偏移纹理的算法,我会非常感兴趣。

过度渲染/渲染不完全
  过度渲染是速度杀手。尤其是复合渲染代码。你绘制,再次绘制,二次重绘,三次重绘,等等。损失时间和速度。一个简单的排除过度渲染的方法是从前到后排列你的三角形,给它们提供Z-Buffer/S-Buffer。这仍然导致问题。完全依赖 Z-Buffer的三角形渲染会最终变成浪费时间。扫描所有的点,而不渲染任何东西!同样的复杂的分段插入S-Buffers代码,也要付出一定代价。 对大的三角形 S-Buffer看起来运作良好。但是,对大量小三角形,它就没那么吃香了;例如,我的S-buffer算法渲染一个不到3k三角形的头部模型,只计算lambert阴影,耗费了接近30秒钟。很明显,三角形数量使得分段插入的代码负载过大。我想,这里的解决方法是开发更高效的闭合和VSD算法。将来处理这个问题有很多可能性。

  另一个问题是“渲染不完全”(我的术语)。这是说,你花费了太多时间在处理离屏的多边形上。一些有帮助的因素是,场景中的可见多边形不会明显变化。到现在为止,你大概可以计算一个FPS中的可见多边形集了,用玩家的方向寻找一些需要注意的多边形,如果玩家靠的太近,就把它们变为临界点。有效范围例如包围球/包围盒在这里也能派上用场。Hierarchial模型也可能有帮助,用来决定哪部分模型是不必要的。

总结
这里有几条规则,可以让你的引擎跑得更快:

尽可能提前计算
不做不必要的计算
不要重复计算同一内容
如果可能,要利用以前计算的结果
寻找那些可以事半功倍的场合
在用汇编改写你的函数前,自问“我已经找到最好的解决方法了吗?”
试验!
冒险。
不要 重写任何代码仅仅因为“看起来慢”
探索你的目标构架,了解它的特性
posted on 2006-03-17 23:33 苦行僧 阅读(665) 评论(0)  编辑 收藏 引用 所属分类: 转载

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理