精度损失和抖颤
本文介绍了 OpenGL 中因渲染巨型尺度场景时出现的精度损失问题,以及随之而来的抖颤现象,深入分析了问题产生的原因,并最终给出了解决相对中心的渲染和相对视角的渲染解决方案。
1. 抖颤
使用OpenGL和Direct3D等图形api,图形处理单元(GPU)在内部采用单精度(32位)浮点数字进行操作,这些数字在很大程度上遵循IEEE 754规范。单精度值通常被认为有7个精确的十进制数字;因此,随着数字越来越大,数字的表示也越来越不精确。在大型场景渲染中,渲染远距离的对象很常见,如果渲染不正确,对象可能会在视觉上发生抖颤(jitter)。当观察者靠近物体时,这个问题变得更加明显。
在航天领域的场景渲染中,精确地放置对象是最重要的。我们希望至少有1cm的精度。允许大约1cm增量的最大数字是131071($2^{17}-1$)m。下面的代码片段显示了代码中正在分配的floatValue
值,相应的注释则为该值实际存储在CPU系统内存中的值。可以看到,虽然存储的值不完全等于指定的值,但误差在0.5厘米内。
1
2
3
4
5
6
7
8
9
10
11
float floatValue;
floatValue = 131071.01; // 131071.0078125
floatValue = 131071.02; // 131071.0234375
floatValue = 131071.03; // 131071.0312500
floatValue = 131071.04; // 131071.0390625
floatValue = 131071.05; // 131071.0468750
floatValue = 131071.06; // 131071.0625000
floatValue = 131071.07; // 131071.0703125
floatValue = 131071.08; // 131071.0781250
floatValue = 131071.09; // 131071.0937500
当将floatValue
再增加1cm时,其真实的指定值与实际存储的值之间的误差将超出1cm,其中一些不同的指定值对应着相同的存储值(代码中标*的位置)。
1
2
3
4
5
6
7
8
9
10
11
float floatValue;
floatValue = 131072.01; // 131072.0156250 *
floatValue = 131072.02; // 131072.0156250 *
floatValue = 131072.03; // 131072.0312500
floatValue = 131072.04; // 131072.0468750 **
floatValue = 131072.05; // 131072.0468750 **
floatValue = 131072.06; // 131072.0625000 *
floatValue = 131072.07; // 131072.0625000 *
floatValue = 131072.08; // 131072.0781250
floatValue = 131072.09; // 131072.0937500
在1:1的场景中,地球的半径可达6378137m,如果需要将一个物体放置在地球表面或者环绕地球的某个轨道上,其位置将远远大于131071m。物体的渲染精度将会变得很差,甚至难以达到分米级的精度。当观察者近距离观察这样一个对象时,该对象将发生抖颤。
如果可以从CPU将双精度(64位)浮点数字传递到GPU,并且GPU内部使用双精度浮点数字操作,那么在地球上或地球附近渲染的对象就不会出现抖动问题。但正如单精度值一样,物体离地球越远,精度就越差,抖动还是会发生。虽然没有进行精确计算,但是如果希望太阳系内的任何东西都能毫无问题地呈现出来,采用双精度浮点数很可能仍然难以胜任。
2. 相对于中心的渲染
90年代STK推出时,人们很快发现,当渲染地球和轨道上的物体(比如飞行器Space Shuttle时),抖动很快就表现出来了。一种解决方案是将对象相对于观察者进行渲染,而不是相对于世界坐标系原点进行渲染。世界空间中靠近观察者的像素区域要比远离观察者的区域小得多;因此,随着观察者接近对象,需要越来越精确地渲染对象且不能产生抖动。相对于观察者渲染对象可提供所需的精度。
首先介绍相对于中心的渲染(Rendering Relative to Center, RTC)技术。RTC技术的核心是重新计算ModelView矩阵,涉及的向量包括:
-
MV_GPU
:从CPU传递到GPU的单精度的ModelView矩阵,用以将飞行器(SpaceShuttle)从世界空间变换至相机空间 -
MV_CPU
:储存在CPU的双精度ModelView矩阵 -
SpaceShuttle_Eye
:飞行器在相机空间中相对于观察者的坐标 -
SpaceShuttle_World
:飞行器在世界空间中的坐标
首先根据Model
矩阵和View
矩阵计算ModelView矩阵MV_CPU
,然后根据行器在世界空间中的坐标计算飞行器相对于观察者的坐标:
1
SpaceShuttle_Eye = MV_CPU * SpaceShuttle_World;
然后,将SpaceShuttle_Eye
赋值给MV_CPU
的平移分量(第四列前三个分量):
1
2
3
MV_CPU[0, 3] = SpaceShuttle_Eye.x;
MV_CPU[1, 3] = SpaceShuttle_Eye.y;
MV_CPU[2, 3] = SpaceShuttle_Eye.z;
最后,将MV_CPU
传递给GPU,得到MV_GPU
,其中存在双精度向单精度的转换:
1
MV_GPU = MV_CPU;
下面采用真实数值来复现上述计算过程。飞行器在世界空间的坐标为
1
SpaceShuttle_World = (16678139.999999, 0.00000, 0.000000)
假设相机位于距离飞行器很近的某处
1
2
3
4
5
MV_CPU = [
0.000000 -0.976339 0.216245 -13.790775
0.451316 -0.192969 -0.871249 -7527123.004836
0.892363 0.097595 0.440638 -14883050.114944
0.000000 0.000000 0.000000 1.000000];
则飞行器相对于相机观察者的相对坐标为
1
2
SpaceShuttle_Eye = MV_CPU * SpaceShuttle_World;
// = (-13.790775, -11.572596, -95.070125)
将上述值插入MV_CPU
的平移分量可以得到:
1
2
3
4
5
MV_CPU = [
0.000000 -0.976339 0.216245 -13.790775
0.451316 -0.192969 -0.871249 -11.572596
0.892363 0.097595 0.440638 -95.070125
0.000000 0.000000 0.000000 1.000000];
最后将MV_CPU
传递给GPU(着色器),得到MV_GPU
。
可以看出,通过上述赋值插入,原本很大的平移分量显著的变小了,这样就可以消除抖颤。一旦视角移动,就需要重新计算MV_GPU
,但是相比渲染成千上万的飞行器顶点而言,计算矩阵的资源消耗几乎可以忽略不计。
3. 相对于视角的渲染
一般而言,RTC技术已经足够达到cm级别的精度,只要模型的顶点距离模型中心小于131070m(实际上可能更小,但暂时未有时间分析测试)。但是在某些极端情况下,RTC技术仍然无法消除抖颤。
如果一个物体的顶点间隔超过131070米怎么办?当绘制卫星轨道线、物体之间的线和几何平面(如赤道平面)时,这并不少见。此时,没有一个可用的中心可以用来防止抖动。此时,需要采用一种改进的技术,即相对于视角的渲染(Rendering Relative to Eye, RTE)。
限于篇幅,此处不再继续展开,有需要可以参看参考文献。
4. 参考文献
[1] STK. Precisions, Precisions