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 进行调整的观察距离变小。

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

下结论:

在几何误差、相机状态是固定值时,只要观察距离 > 计算此几何误差的经验距离,就会渲染下一级瓦片,此瓦片若是 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 \]

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

posted @ 2020-07-21 20:30  秋意正寒  阅读(304)  评论(0编辑  收藏