Doom3 平方根求解算法的个人领悟 欢迎来讨论

最近在看 Doom3 Source Code,最先看的就是idLib里的math,各种高效牛叉算法,很多都是比较底层的,如基本数据类型在内存中的存储方式,尤以 int 和 float 为突出,这里先摆上自己的对于其中的平方根的求解算法的个人分析,主要不是给那些衣来伸手、饭来张口的人看的,这些东西还是要自己先琢磨琢磨才好玩,希望有人可以来一起讨论讨论,还是很有意思的。

废话不多说了,开工:Doom3里id采用了结合查询表的方式实现高效选择后面牛顿迭代的逼近直,先看相关的宏定义和变量定义:

private:

enum {

LOOKUP_BITS = 8,

EXP_POS = 23,

EXP_BIAS = 127,

LOOKUP_POS = (EXP_POS-LOOKUP_BITS),

SEED_POS = (EXP_POS-8),

SQRT_TABLE_SIZE = (2<

LOOKUP_MASK = (SQRT_TABLE_SIZE-1)

};


union _flint {

dword i;

float f;

};


static dword iSqrt[SQRT_TABLE_SIZE];

static bool initialized;

};


(注:本人实在是太懒了)

上面的枚举类型主要是IEEE float的相关存储方式(其实我一直很纳闷:0.0f是如何存储的,查看内存地址,全是0,但是按照float的转化方式,二进制应该是 0 01111111 00000000000000000000000,但是这样再转化到实数就是 1.0 * ( 2.0 ^ 0 ) = 1.0, 可能是底层加了转化判断,有大虾知道吗),至于其他的如 LOOKUP_BITS LOOKUP_POS,还有为什么查询表的大小SQRT_TABLE_SIZE = 512 = 2 ^ 9,后面用的时候再说:

这个是idMath的初始化函数,主要就是初始化平方根的查询表,前面已经看到_flint联合体:

union _flint {

dword i;

float f;

};

typedef dword unsigned int;

这个挺奇妙的,是直接取地址读取,而不是类型转化,C++里是有强类型转化reinterpret_cast也可以转化地址,因为float类型是不能直接进行位操作的,需要转化为int.

这个查询表是从float的尾数的8位以及指数位的最低位为索引,其实float实数 = ( 1.0 + M )* ( 2 ^ ( E - 127 )),M位尾数,E为指数

因为float的指数是加上127存储于内存中的,故前面

fi.i= ((EXP_BIAS-1) << EXP_POS) | (i << LOOKUP_POS);//填充尾数的前8位以及和指数的最低位加上126的指数,转化所需的float

fo.f= (float)( 1.0 / sqrt( fi.f ) ); //使用了库函数sqrt求平方根

iSqrt[i] = ((dword)(((fo.i + (1<<(SEED_POS-2))) >> SEED_POS) &0xFF))<//去掉多余项,保留8位尾数

可以算算,这个区间的数值的平方根的倒数都在[1.0, 2.0)之间,所以指数位为0

iSqrt[SQRT_TABLE_SIZE /2] = ((dword)(0xFF))<<(SEED_POS); //对1.0进行格外处理

可以看到其实区间[0, 255]是区间[256, 511]的 2.0 ^ -0.5,继续


这个是实际的求一个数的更方根的倒数:

dword a = ((union _flint*)(&x))->i;//转化为_flint方便进行位操作

union _flint seed;//索引所得直


assert( initialized );//确保已经查询表已经初始化


double y = x *0.5f; //用于后面的牛顿迭代,使用double增加精度的节奏吗

seed.i = (( ( (3*EXP_BIAS-1) - ( (a >> EXP_POS) &0xFF) ) >> 1)<> (EXP_POS-LOOKUP_BITS)) & LOOKUP_MASK];

double r = seed.f;

r = r * (1.5f - r * r * y );//牛顿迭代,泰勒级数相关求解

r = r * (1.5f - r * r * y );//同上,二次迭代,增加逼近精度

return (float) r;

重点这一行:

seed.i = (( ( (3*EXP_BIAS-1) - ( (a >> EXP_POS) & 0xFF) ) >> 1)<> (EXP_POS-LOOKUP_BITS)) & LOOKUP_MASK];

一个float的平方根的倒数是等于(从存储来看) = 

(( 1.0 + M )* ( 2 ^ ( E - 127 ))) ^ -0.5 

= ( 1.0 + M ) ^ -0.5 * ( 2 ^ ( ( E - 127 ) * -0.5 ) )

对于指数E其实进行的是 -( E - 127 )/2,最终存储回内存还是加上127,也就是 ( 3 * 127 - E )* 0.5,但是对于移位而言,在是奇数指数的情况下是会舍弃后面的0.5,这也就是为什么,查询表的索引包括了指数位的最低位,以及这(3*EXP_BIAS-1)减去了1,之后把将其移到指数的位置,与上索引的直:

iSqrt[(a >> (EXP_POS-LOOKUP_BITS)) & LOOKUP_MASK]

获取索引,也就是指数的最低位和尾数的头八位(这个好像说了好几遍了,有点蛋疼了),与上掩码去掉多余位

就这样得到了理想的平方根的逼近直,在进行两次牛顿迭代,就更接近,经过测试,精度不下于库函数sqrt,性能优于sqrt,至于快多少,没有仔细比较函数消耗

时间,最后,有个我还是挺困惑的就是

iSqrt[i] = ((dword)(((fo.i + (1<<(SEED_POS-2))) >> SEED_POS) & 0xFF))<


这里,为什么要加上(1<<(SEED_POS-2)),it does really make me confused! 只有第13,14位为1时会有影响,希望有大侠给点指点

至于很多细节部分,我并没有详细讲解,给了点方向,主要还是要靠自己领悟琢磨才有意思,

你可能感兴趣的:(菜鸟写游戏引擎)