您的位置:首页 > 其它

游戏引擎研究 —— 单位向量与八面体的转换算法

2017-03-07 23:48 399 查看
这篇博客介绍了在延迟渲染(Deferred Rendering)中比较常用的一种用于优化单位向量在G-Buffer中储存的算法,该算法在Quake2、虚幻Ⅳ引擎中被运用。

读者可以在虚幻引擎Ⅳ的
DeferredShadingCommon.usf
中接触到相应的代码,也可以阅读相关论文,传送门

单位向量

在计算机图形学中单位向量的使用极为普遍,例如表面的法线向量、切线向量和光线相关的向量等。这样一来,很多的图形学的算法的时间&空间性能都取决于针对这些向量的读取、处理和写入的速度。

特别的是传输这些数据的带宽需求与G-buffer对于内存的需求在很多地方制约了GPU渲染的速度。

在现在的图形渲染管线中,3D的向量要么存在高带宽的寄存器(registers)中,要么存在中等带宽的计算机内存中,要么存在低带宽的磁盘中。针对于单位向量,寄存器由于对速度有着极高的要求,往往希望你能够将其以
float32*3
的形式进行单位向量的储存,而磁盘往往更希望更有效地利用存储空间,因此会鼓励你进行各种的编码/解码操作。

但是针对于在内存(或者芯片上的cache)中的存储,往往需要在速度和存储空间上取得一个平衡。因此可以考虑将单位向量进行某种程度的编码和解码,当然这种编码和解码也不能太复杂或者损失太多信息。

问题的提出和思路

这个算法的目的是如何编码一个单位向量的同时,能够让其占用最少的位数以及让信息损失量达到一个可接受的程度;或者说如何在一个固定的带宽限制下,尽量最小化损失的信息。

正常的表现方法以及分析

先看看单位向量的最普通的表现方法:一个包含三个32-bit的浮点数的结构体:

struct{float x, y, z;}


那么它所占用的储存空间为:3∗4∗8=96bits。

可知的是,这种表现方法的域实际上是整个3D空间,R3,换句话说就是从原点到无穷远处(也不尽然……32位浮点数也有范围)的空间。

但是正常的单位向量的域却是位于原点,半径为1的球面。

这就意味着这96-bit位的模版中有绝大部分的实例针对于表现单位向量是不起任何作用的。换句话说,资源浪费了不少

就存储空间来说,完美的模版则应该是每一种实例都能够表现这个球面上的唯一的一个点。

容易想到的优化算法

有一种容易想到的算法是使用球面坐标,也就是存储两个32-bit的浮点数用于存储该向量在三维坐标系中的角度。这样一来只需要花费64个位就能够进行储存了,而且精度相对来讲也更加高。但是这要求进行一系列的三角函数运算,在计算过程中会带来更多的误差,也会导致导致更多的计算量。

此外,这样的做法会导致在两极的点较为密集、而在赤道上的点较为稀疏,这样的偏差在很多地方也会出现问题。

真正完美的方法应该是兼顾效率、误差、编码、解码和保证其均匀分布各个方面。

转化为八面体的算法

有一种可行的算法是将整个球面上的点先投影至一个八面体中,然后再投影到z=0的平面上。

但是针对于那些z<0的点,则将其针对于八面体里z=0的那些边进行对折,从而将圆的点映射到一个平面上,该平面为长宽为2的正方形,过程图如下:



这样一来,我们只需要储存对应的(u,v)坐标即可。

我们认为这种方法相对来讲是最好的:编码解码的计算过程简单,而且基本上也是平均的分布。

针对于这种向量进行编码的方式叫做“Octahedral Normal Vectors (ONV),”这种方法相对来讲高效且优雅,并且误差也较小。

上面提到计算过程简单的原因其实一开始我也没想明白,而且也计算了好一会。但是后来发现在真正的实现中也并没有按照完全的几何结果来计算,而是粗暴的将L2(欧几里德范数)代替为L1(曼哈顿范数),从而得到一个近似的解。

按照上面的方法来实现的话,那么对应的编码和解码如下:

float2 UnitVectorToOctahedron( float3 N )
{
N.xy /= dot( 1, abs(N) );
if( N.z <= 0 )
{
N.xy = ( 1 - abs(N.yx) ) * ( N.xy >= 0 ? float2(1,1) : float2(-1,-1) );
}
return N.xy;
}

float3 OctahedronToUnitVector( float2 Oct )
{
float3 N = float3( Oct, 1 - dot( 1, abs(Oct) ) );
if( N.z < 0 )
{
N.xy = ( 1 - abs(N.yx) ) * ( N.xy >= 0 ? float2(1,1) : float2(-1,-1) );
}
return normalize(N);
}


实际上也有更加精确的实现方法,然而可能相对来讲计算量要更大一些。GLSL代码如下:

vec2 float32x3_to_octn_precise(vec3 v, const in int n)
{
vec2 s = float32x3_to_oct(v); // Remap to the square
// Each snorm’s max value interpreted as an integer,
// e.g., 127.0 for snorm8
float M = float(1 << ((n/2) - 1)) - 1.0;
// Remap components to snorm(n/2) precision...with floor instead
// of round (see equation 1)
s = floor(clamp(s, -1.0, +1.0) * M) * (1.0 / M);
vec2 bestRepresentation = s;
float highestCosine = dot(oct_to_float32x3(s), v);
// Test all combinations of floor and ceil and keep the best.
// Note that at +/- 1, this will exit the square... but that
// will be a worse encoding and never win.
for (int i = 0; i <= 1; ++i)
for (int j = 0; j <= 1; ++j)
// This branch will be evaluated at compile time
if ((i != 0) || (j != 0))
{
// Offset the bit pattern (which is stored in floating
// point!) to effectively change the rounding mode
// (when i or j is 0: floor, when it is one: ceiling)
vec2 candidate = vec2(i, j) * (1 / M) + s;
float cosine = dot(oct_to_float32x3(candidate), v);
if (cosine > highestCosine)
{
bestRepresentation = candidate;
highestCosine = cosine;
}
}
return bestRepresentation;
}


由于这就和各种标量的表现方法相关了,这里不再赘述。读者有意愿了解的话可以去阅读对应论文。

<全文完>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息