zoukankan      html  css  js  c++  java
  • 压缩tangent frame

    转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=2038

    (对tangent frame的压缩,其实在2008年做All-frequency rendering of dynamic, spatially-varying reflectance这篇paper的过程中,就曾在茶余饭后讨论过这个事情。这其实是个很trivial的问题,但看到Crytek和Avalanche分别在两年的SIGGRAPH talk上都煞有介事的提到这个问题,才想写这篇blog介绍一下。)

    Avalanche Studios的Emil Persson,也就是Humus,在SIGGRAPH 2012上的talk Creating Vast Game Worlds中提到了他们在游戏中如何压缩顶点数据,其中对于tangent frame的压缩与去年Crytek的Spherical Skinning with Dual-Quaternions and QTangents的压缩方法极其相似。但在两者的介绍中,由于某些原因,总有一些细节的陷阱没有提到。然而,在实际使用中,对付这些陷阱上所花的时间往往会超过实现方法本身。所以,本文试图通过对整个方法以及细节陷阱的描述,集中地把这个问题阐释清楚,希望对有兴趣的读者有所帮助。

    Tangent frame

    在现代游戏中,要做per-pixel lighting少不了用到tangent frame。完整的tangent frame由tangent、binormal和normal三个向量来表示。如果每一个向量都用保存,并且每个分量都用float的话,就需要3*3*4=36字节。对于vertex来说,这个消耗是巨大的。

    压缩到28字节

    最直接的想法就是,因为tangent、binormal和normal是正交的,所以只要保存其中2个,另一个在runtime通过cross计算出来就行了。(至于要省略哪个其实都一样,但由于KlayGE里省略的是binormal,本文就按照这一点来描述。)这样就压缩到了2*3*4=24字节,但是要注意一个大陷阱。

    陷阱1

    很多artist在做对称物体的纹理坐标时,都是只做了一半的纹理坐标,然后通过max或者maya的镜像方式布上另一半。在这种情况下,cross(normal, tangent)的结果是-binormal,而不是binormal。如果不注意这点,镜像部分的normal map就会得到相反的结果,如Crytek的这张图所示,左边的考虑了镜像,右边的没考虑:

    Mirror

    所以除了tangent和normal,还需要多保留一个称为reflection的值,取-1或+1。在计算binormal的时候,也就成了binormal = cross(normal, tangent) * reflection。所以为了和原始的tangent frame达到一样的表达能力,最终需要(2*3+1)*4=28个字节。Avalanche Studios的方法完全忽略了这个问题,他们估计只能通过限制artist的操作来回避了。

    压缩到8字节

    28字节对于vertex来说仍然太大,而且很明显可以对其做进一步压缩。因为tangent和normal都是单位向量,而且精度要求没那么高,每个分量都用float来保存实属浪费。最贴近的格式是A2BGR10。这种格式的BGR都是10-bit,A是2-bit,对于tangent来说,RGB分别存xyz,A存reflection。对于normal来说,舍弃A也只是浪费2-bit。如果硬件不支持A2BGR10,用ARGB8也可以,肉眼基本看不出区别。通过这种方式,tangent frame被压缩到了8个字节。这也是KlayGE 4.1之前使用的存储方法。

    压缩到4字节

    8字节已经比一开始少得多了,还能做进一步压缩吗?能,我们的目标是4字节!tangent和normal的方法,在8字节已经走到头了,我们需要回到原点,回到原始的tangent frame表达。从本质来说,tangent frame表示的是一个局部坐标系的旋转(当然,还有可能有镜像),和原点本身的位置无关,很自然可以想到用quaternion来表示tangent frame。一个正交的3×3的matrix,可以通过quaternion用4个分量无损地表达出来。这正好适合我们的要求,但也需要注意几个大陷阱。

    陷阱1

    把matrix转成quaternion的时候,默认的假设是matrix是正交的。也就是说,你的tangent frame必须正交。但因为在建立tangent frame的时候,往往是先单独算normal,在生成tangent和binormal,正交并不一定能直接得到完全的保证。所以在计算出tangent的时候需要额外做一次正交化:

    tangent = normalize(tangent - normal * dot(tangent, normal));

    经过这步操作,tangent和normal就互相垂直了,符合quaternion的条件。注意上式的normal需要事先normalize过。

    陷阱2

    这里仍然有个镜像的问题。送去生成quaternion的matrix必须去掉reflection,在得到quaternion之后再把reflection加回去。注意到quaternion有个很好的性质,就是q = -q(q的类型是quaternion),所以我们就可以开始打.w分量的主意了:

    if (q.w < 0)
        q = -q;
    if (reflection < 0)
        q = -q;

    经过这两个if,reflection就保存到了q.w的符号位上,而q本身表示的旋转没有改变。在shader中,只需要简单地:

    reflection = q.w < 0 ? -1 : +1

    就可以恢复出reflection。

    陷阱3

    如何把quaternion保存到4字节?做法之一是和前面一样,用A2BGR10的格式,RGB分别保存xyz,A保存符号。因为quaternion是归一化的,所以可以在vertex shader中通过计算来恢复w。这么做的优点是精度较高,缺点是计算量增加了。

    陷阱4

    能不能就把quaternion保存到ARGB8?很可惜,还是有个陷阱。如果完全按照IEEE的浮点规范,0也是有+0和-0的区别的,所以即便q.w == 0,q.w的符号照样可以用来保存reflection。但如果保存到8-bit的通道,就已经不再是浮点数,所以不会在遵循那个规则,+0和-0都会变成+0。为了保住q.w的符号,这里必须保证q.w不等于0,但也不能大到影响q本身。(Crytek认为这是因为GPU不完全遵守IEEE浮点规范造成的,但很显然,保存到8-bit之后就和浮点规范无关了。)对于需要量化到8-bit的情况下,最小的值就等于1/127。同时,为了保证q仍然是归一化的,需要把xyz三个分量都乘上一个系数:

    float bias = 1.0f / ((1UL << (8 - 1)) - 1);
    if (q.w < bias)
    {
        q.xyz *= sqrt(1 - bias * bias);
        q.w = bias;
    }

    最终,我们得到了一个既不浪费计算量,又能把tangent frame压缩到4字节的方法。

    红利

    用quaternion来表示tangent frame,除了能压缩数据之外,还有一些额外的红利。如果用了dual quaternion形式的骨骼动画,可以在带权累积DQ之后,直接乘上tangent frame的quaternion,而不必把quaternion解出tangent、binormal和normal后分别计算骨骼变换。

    未来

    在Crytek的ppt里,还提到了一些未来可能能做的事情。包括了quaternion的传递和把quaternion保存在G-Buffer。这两者其实都涉及到插值的问题。目前硬件在VS/GS->PS的时候,可以分属性选择线性插值、不插值、不透视矫正,但quaternion的插值是圆周插值,硬做线性插值会导致错误的结果。所以如果在未来的硬件中,或者像salvia这样的软件光栅化渲染器中,提供了slerp这样的插值方式,就能顺利地把tangent frame的quaternion传递到PS,并且能保存到G-Buffer中。如果G-Buffer保存的是完整的tangent frame,而不光只有normal,就可以在计算lighting的时候使用更为复杂的各向异性BRDF。

  • 相关阅读:
    Effective Java 19 Use interfaces only to define types
    Effective Java 18 Prefer interfaces to abstract classes
    Effective Java 17 Design and document for inheritance or else prohibit it
    Effective Java 16 Favor composition over inheritance
    Effective Java 15 Minimize mutability
    Effective Java 14 In public classes, use accessor methods, not public fields
    Effective Java 13 Minimize the accessibility of classes and members
    Effective Java 12 Consider implementing Comparable
    sencha touch SortableList 的使用
    sencha touch dataview 中添加 button 等复杂布局并添加监听事件
  • 原文地址:https://www.cnblogs.com/gongminmin/p/2701350.html
Copyright © 2011-2022 走看看