3dTiles 几何误差详解


转载请注明出处。全网@秋意正寒

1. 瓦片的调度

查阅 tileset.json 的规范,有一个属性是 refine,它有两个值:"ADD""REPLACE"

还有另一个属性,叫 geometricError,是一个数字。

"ADD" 的含义是,当这一级瓦片显示不够精细时,渲染下一级瓦片,这一级的瓦片保留继续显示(增加下一级的内容)。

"REPLACE" 的含义是,当这一级瓦片显示不够精细时,渲染下一级瓦片,这一级的瓦片被销毁(被下一级“替换”)。

如何衡量这个“不够精细”?

一个很简单的思路是利用观察点(也就是相机)到观察瓦片的距离来判断。这个相机与瓦片的距离超过我指定的某个阈值的时候,就要渲染下一级瓦片,而这一级瓦片则根据 refine 的值进行保留或销毁。

所谓的 “指定的某个阈值”,在这里有一个专有名词:最大屏幕空间误差(maximumScreenSpaceError)。

这个值是 Cesium3DTileset 类中的实例属性,默认值是16.

暂且不说这个16的具体含义,先回顾刚才的思路:计算相机到瓦片的距离,设为distance,就能与这个值进行比较了吗?不是的。

1.1 屏幕空间误差(ScreenSpaceError, sse)

计算当前瓦片的屏幕空间误差值,才能与 maximumScreenSpaceError 进行比较,因为这两个才是同一种东西嘛。

先说结论:屏幕空间误差(ScreenSpaceError, sse)由几何误差、相机状态有关的各项参数计算而来。

也就是说,只要 Cesium 在跑,这个 sse 就是一帧一帧实时计算的,每时每刻都在计算。

查阅 Cesium3DTile 的源码,不难得知它的计算方法被定义在 Cesium3DTile 中(可以跳过代码不看):

// Cesium3DTile.js >> Cesium3DTile.prototype.getScreenSpaceError()
Cesium3DTile.prototype.getScreenSpaceError = function (
  frameState,
  useParentGeometricError,
  progressiveResolutionHeightFraction
) {
  var tileset = this._tileset;
  var heightFraction = defaultValue(progressiveResolutionHeightFraction, 1.0);
  var parentGeometricError = defined(this.parent)
    ? this.parent.geometricError
    : tileset._geometricError;
  var geometricError = useParentGeometricError
    ? parentGeometricError
    : this.geometricError;
  if (geometricError === 0.0) {
    // Leaf tiles do not have any error so save the computation
    return 0.0;
  }
  var camera = frameState.camera;
  var frustum = camera.frustum;
  var context = frameState.context;
  var width = context.drawingBufferWidth;
  var height = context.drawingBufferHeight * heightFraction;
  var error;
  if (
    frameState.mode === SceneMode.SCENE2D ||
    frustum instanceof OrthographicFrustum
  ) {
    if (defined(frustum._offCenterFrustum)) {
      frustum = frustum._offCenterFrustum;
    }
    var pixelSize =
      Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) /
      Math.max(width, height);
    error = geometricError / pixelSize;
  } else {
    // Avoid divide by zero when viewer is inside the tile
    var distance = Math.max(this._distanceToCamera, CesiumMath.EPSILON7);
    var sseDenominator = camera.frustum.sseDenominator;
    error = (geometricError * height) / (distance * sseDenominator);
    if (tileset.dynamicScreenSpaceError) {
      var density = tileset._dynamicScreenSpaceErrorComputedDensity;
      var factor = tileset.dynamicScreenSpaceErrorFactor;
      var dynamicError = CesiumMath.fog(distance, density) * factor;
      error -= dynamicError;
    }
  }

  error /= frameState.pixelRatio;

  return error;
};

这么长,其实在我们关心的三维模式(即 frameState.mode 为 SceneMode.SCENE3D)下,最核心的只有一句代码:

error = (geometricError * height) / (distance * sseDenominator);

其中,

  • error 即计算得到的 sse 屏幕空间误差
  • geometricError 即当前瓦片设置好的几何误差,写在 tileset.json 中
  • height 即浏览器当前运行着 Cesium 的那个 canvas 的像素高度,如果没有自己设置 progressiveResolutionHeightFraction 值,通常 height 值就是canvas 的像素高度,如果你的 Cesium 占据了全屏,你的显示器分辨率是 1920 × 1080,那么这个 height 在你浏览器全屏时,通常是 936 像素。
  • distance 是当前状态下,摄像机的世界坐标位置到瓦片中心位置的距离,单位是米
  • sseDenominator 是一个根据当前相机状态下,根据视锥体的张角(fov)、长宽比参数进行一系列三角计算、四则运算而来的一个参数,具体含义我没有深究,但是通常状态下,很少会去修改默认相机的参数,即张角 60 度,宽高比就是 1920÷936(就是canvas的像素宽高比啦),所以这个值也是固定的,有兴趣的读者可以跟踪这个参数的计算过程,还要往里套五六层代码才知道计算过程。它翻译过来就是“sse的分母”。

2. 推演

如果不对相机进行修改,使用默认的,而且你的屏幕是1920×1080,恰好你的 canvas 占满了 body,而且你的浏览器是最大化的状态,那么这个 sseDenominator 的值约为 0.5629165124598852

显而易见,为了屏蔽屏幕分辨率差异、浏览器是否最大化的差异,这个 sseDenominator 的值是会根据浏览器窗口状态、canvas大小以及摄像机的状态进行变化的。在此,我们假定就是 0.5629165124598852

那么 上述代码改写成:

\[sse = \frac{geometricError×936}{distance × 0.5629165124598852} \]

是否还记得一个参数:maximumScreenSpaceError?它的默认值是16

那么,这个16就是一个临界值,当 \(sse < defaultMaximumScreenSpaceError = 16\) 时,下一级瓦片加载,此瓦片根据 refine 进行调整。

假设 sse 刚好等于16,那么得到一个二元方程:

\[16 = \frac{geometricError×936}{distance × 0.5629165124598852} \]

所以,这个等式表达的含义就是,当几何误差越大(分子变大),要想等式保持相等,那么分母:distance(相机到瓦片的距离)也应变大,变大就意味着——根据 refine 来控制瓦片的显隐或增补时,此瓦片观察距离由此变大。

所以,这个几何误差是一个经验值。

下结论:

在几何误差、相机状态是固定值时,只要观察距离 > 计算此几何误差的经验距离,就会渲染下一级瓦片,此瓦片的 refine 规则若是 REPLACE 则消失,若是 ADD 则保留。
若渲染下一级瓦片,则当前瓦片的 sse 必定 < maximumScreenSpaceError。

3. 经验值下的几何误差计算

还是以刚好到临界值,也即默认的 16 时,为例。

设定某瓦片距离相机超过200米时,该瓦片到达临界状态。

代入上式,计算得到 geometricError 为:

\[geometricError = 200 × 0.5629165124598852×16÷936=1.9245008972987 \]

那么现在这个瓦片的 sse 公式变成了:

\[sse = \frac{1.9245008972987×936}{distance×0.5629165124598852} \]

也就是距离越大,sse 越小。当距离超过200米,sse一定小于16,不妨设 refineREPLACE

在视图中观察到小于200米时,此瓦片正常显示,距离一旦大于200米,该瓦片就被 REPLACE 了,此时的 sse 肯定也小于16,只需调整 maximumScreenSpaceError,在 CesiumLab 中叫显示精度,调小一些,该瓦片又被显示了。

不妨假设就按 1080p屏幕 + 全屏canvas + 最大化浏览器窗口 + 默认相机参数来算,列举常见观察距离的几何误差设置:

观察距离 几何误差
100 0.96225045
200 1.92450090
300 2.88675134
400 3.84900179
500 4.81125224
1000 9.62250447
2000 19.24500897

观察不难得知,这是一个一次函数:

\[geometricError =f(distance) = distance × 0.5629165124598852×16÷936 \]

而后面三个数字,则与相机、浏览器等因素有关,只要浏览器不变,相机不变,显示器不变,那么无论怎么操作视图,几何误差的计算都只跟经验上的观察距离有关。

4. 调参经验

4.1. 当前视角如果不继续放大,倾斜摄影的层级比较低(看起来模糊)怎么办

方案① 调整最大屏幕空间误差

调大 maximumScreenSpaceError,因为几何误差不变、距离不变,等式左边放大,势必等号要变成大于号:

\[maximumScreenSpaceError > 16 = \frac{geometricError×936}{distance × 0.5629165124598852} \]

根据上述结论,同等条件下最大屏幕空间误差变大,导致计算结果 小于 最大屏幕空间误差,从而引发下一级瓦片渲染,当前瓦片执行 "refine" 策略。

方案② 根治法:调整几何误差

不渲染下一级瓦片的原因无非是当前几何误差所代表的经验距离太大了,不小于这个距离就没法渲染。
解决方法很简单,把几何误差调小 即可,参考上文。

4.2. 想不加载这么快

同 4.1. 的思路,减小 maximumScreenSpaceError增大 几何误差。