产生抖动现象的原因
使用OpenGL和Direct3D等图形API绘图时,图形处理单元 (GPU) 在内部以32位单精度浮点数进行运算。在IEEE 754规范中,单精度值通常有7位精确的有效数字,因此,当传入GPU的数值变得越来越大时,数值的表示就会越来越不准确。
三维数字地球往往要在大尺度下渲染物体,如果数值精度处理不当,物体看起来会有抖动的现象。假如我们要确保至少有1厘米的精度,那么传给GPU的最大数值是131,071(允许大约1厘米增量的最大数值是 - 1,即131,071 )。如果物体的坐标是相对于地球中心的,则需要远大于131,071米的数值才能将物体放置在地球表面附近,那样,物体位置的精度就达不到厘米级,靠近物体观察就会出现抖动。
GPU上以单精度数值计算的限制我们是绕不过去的,但是可以通过数学方法避免直接将数值过大的坐标直接传给GPU造成精度损失。
渲染抖动现象
解决方案
方案一:在局部坐标系下渲染物体
以几何体附近的一个点(通常是一个顶点或中心点)作为原点构建局部坐标系,局部坐标系相对于世界坐标系有一个4×4的模型矩阵,用这个模型矩阵的逆矩阵依次乘上每个顶点的世界坐标,将顶点坐标转换到局部坐标系下。将局部坐标系下很小的坐标值传递给GPU用单精度表示就不会出现精度损失了。
1 | -0.862626220291405 | 0.25850359033743103 | -0.43480098648510707 | -2776429.768638609 |
---|---|---|---|---|
2 | -0.5058418765442069 | -0.44083336197462497 | 0.7414782147200178 | 4734722.901131647 |
3 | 0 | 0.8595590967192467 | 0.5110363580482244 | 3241386.8326024916 |
4 | 0 | 0 | 0 | 1 |
局部坐标系相对于世界坐标系的模型矩阵尺度
x | y | z | |
---|---|---|---|
世界坐标 | -2776435.331977081 | 4734722.6395306345 | 3241382.4787405613 |
局部坐标 | 4.931410385295749 | -5.065222142737184 | -0.000008139759302139282 |
世界坐标和局部坐标的尺度
细心的同学可能发现了,你这模型矩阵右上角三个偏移量也是很大的数值,传给GPU做乘法运算不会有精度损失吗?当然会!但我们通常是把模型视图矩阵(MV)或者模型视图投影矩阵(MVP
)传给GPU。
1 | 0.9852994086965299 | 0.17083639899696584 | 0 | -0.03417507361155003 |
---|---|---|---|---|
2 | -0.1706695893268321 | 0.9843372984888255 | 0.044181150762873656 | 82.30410098424181 |
3 | 0.00754761560224626 | -0.043531684799359815 | 0.9990235362178661 | 1765.9144354593009 |
4 | 0 | 0 | 0 | 1 |
模型视图矩阵(MV)的尺度
1 | 1.706588636529962 | 0.29589732284485365 | 0 | -0.059192963847611066 |
---|---|---|---|---|
2 | -0.5857256223326083 | 3.3781740438741545 | 0.15162649730408945 | 282.46169080071627 |
3 | -0.007700077283409937 | 0.044411023944085526 | -1.019203791265371 | -202001597.54567307 |
4 | -0.00754761560224626 | 0.043531684799359815 | -0.9990235362178661 | -1765.9144354593009 |
模型视图投影矩阵(MVP)的尺度
可是模型视图投影(MVP)矩阵中也有很大的数值(比如上面MVP矩阵第三行第四列),为什么实际中不会抖动呢?确实有精度损失,但视觉上影响不大,我们看不出来而已。用上面的MVP矩阵做一个实验,对第三行第四列那个很大的值做一定增量,看看转换得到的NDC坐标(标准化设备坐标)的差异(NDC坐标和屏幕坐标是关联的)。如下表所示,不同增量下转换得到的NDC坐标 x、y 分量一致,只有 z 分量有很小的差异,因此在屏幕上看不出差别。
原始局部坐标 | MVP[3][4]的增量 | 转换得到的NDC坐标 | ||
---|---|---|---|---|
x: 4.931410385295749 y: -5.065222142737184 z: -0.000008139759302139282 | ||||
0 | x: -0.003380723663075517, y: -0.5243922209020915, z: 115765.780247093, w: 1 | |||
1 | x: -0.003380723663075517, y: -0.5243922209020915, z: 115765.77967399955, w: 1 | |||
10(为了实验夸大了精度损失) | x: -0.003380723663075517, y: -0.5243922209020915, z: 115765.77451615849, w: 1 |
上述MVP矩阵大数值对齐次坐标的影响
我们同样可以做另一个实验,验证把世界坐标传给GPU的精度损失对NDC坐标的影响。如下表所示,世界坐标的精度损失对NDC坐标的影响是比较大的,因此抖动会很明显。
原始世界坐标 | xyz的增量 | 转换得到的NDC坐标 |
---|---|---|
x: -2775649.009164771 y: 4733391.45028682 z: 3240469.1812303886 | 0 | x: -0.0021130884060659803, y: 0.36409925203167576, z: -7213704.717357739, w: 1 |
1 | x: -0.08840036007138063, y: 0.3764750100577717, z: -7292874.635747884, w: 1 | |
10(为了实验夸大了精度损失) | x: -1.058237819443789, y: 2.0922624411469486, z: -9984583.083156265, w: 1 |
世界坐标传给GPU精度损失对NDC坐标的影响
该方案适用于中小范围区域内的渲染,如果渲染物体很大,大到顶点距离局部坐标系中心点的距离超过了保证精度的阈值,比如赤道平面,那么依然会出现抖动。当然也可以将赤道平面这种超大的几何体细分成很多三角形,然后在每个三角形的局部坐标系下渲染,但细分三角形会耗费不少性能,而且每个细分三角形都在各自的局部坐标系下渲染一遍会调用太多绘制命令,是很不划算的。
方案二:相对相机渲染物体
浮点数由三部分组成:1个符号位、8个指数位和23个小数位。在这种方案中,首先在 CPU 上把每个双精度数编码为两个单精度浮点数:一个高位浮点数,一个低位浮点数。
在低位浮点数中,23个小数位中的7个用于双精度数小数点后的部分,意味着数值的精度为,0.0078125,这比1厘米还小。其余 16 位用于表示从 0 到 65535 (
- 1) 的整数值,增量为 1。
高位浮点数使用全部 23 位来表示 65,536 到 549,755,748,352 (( - 1) * 65536)范围内的数值,增量为 65536。
void CDoubleToTwoFloats::Convert(double doubleValue,
float& floatHigh, float& floatLow)
{
if (doubleValue >= 0.0)
{
double doubleHigh = floor(doubleValue / 65536.0) * 65536.0;
floatHigh = (float)doubleHigh;
floatLow = (float)(doubleValue - doubleHigh);
}
else
{
double doubleHigh = floor(-doubleValue / 65536.0) * 65536.0;
floatHigh = (float)-doubleHigh;
floatLow = (float)(doubleValue + doubleHigh);
}
}
将一个双精度浮点数编码为两个单精度浮点数
在GPU的顶点着色器阶段,顶点位置和相机位置的高位差值与低位差值分别计算,以保持各自的精度。用相对于相机的模型视图投影矩阵()把高低位差值之和表示的偏移量转换为裁剪坐标系下的位置。其中相对于相机的模型视图投影矩阵是在正常的模型视图投影矩阵的基础上把右上角的3个偏移量都置为0,即以相机为坐标系原点来进行坐标空间的变换。
// 相机的世界坐标编码为的两个单精度浮点数
uniform vec3 uEncodedCameraPositionMCHigh;
uniform vec3 uEncodedCameraPositionMCLow;
// 相对于相机的模型视图投影矩阵
uniform mat4 uModelViewProjectionRelativeToEye;
void main(void)
{
vec3 highDifference = high - uEncodedCameraPositionMCHigh;
vec3 lowDifference = low - uEncodedCameraPositionMCLow;
gl_Position = uModelViewProjectionRelativeToEye *
vec4(highDifference + lowDifference, 1.0);
}
在顶点着色器中将高位和低位两个单精度浮点数转换为裁剪坐标
该方案的缺点是位置数据的由一个浮点数加倍到两个浮点数,从CPU传到GPU的时间会相应变长,并且会占用更多的显存空间。