cesium源码解读:primitive、entity贴地原理解读

篇幅有点长。。请大家耐心观看。

附上经过处理后pointPrimitive贴地的视频。

  cesium.js中贴地的核心文件:

只需要了解以下几个文件就能知道如何对点进行贴地。

1. Scene.js: updateHeight、getHeight    private函数。

1.Scene.prototype.updateHeight = function (
  cartographic,
  callback,
  heightReference,
)

2. getHeight = function (cartographic, heightReference)

2.QuadtreePrimitive.js : updateHeights 、 updateHeight 函数

1.updateHeight = function (cartographic, callback)

2. updateHeights(primitive, frameState)

3.selectTilesForRendering(primitive, frameState)

3.QuadtreeTile.js : _updateCustomData

_updateCustomData = function (
  frameNumber,
  added,
  removed,
)

大概流程

cesium中贴地的逻辑是,这里以 HeightReference .CLAMP_TO_GROUND 贴地形为例。

1.使用scene.getHeight() 函数 获取当前点位的地形高度。注意:每个层级的地形高度都不一样,所以只能获取当前层级的地形高度。也正是因为这个原因才需要实时的贴地处理。

2.把该点位以及更新点位的函数存起来。

3. 当某个地形瓦片加载的时候,会从某个数组中拿到需要贴地的点。然后判断该点是否在这个瓦片的范围内。

4.若在范围内,则调用第2点中的更新点位回调,会重新调用scene.getHeight()的方法。并把获取到的高度重新赋值给你的要素。

总结:就是地形瓦片加载上地图时会被监听到。然后拿到需要贴地的点在根据点的位置实时更新点位。这样的好处是只有地形瓦片被加载时才会更新点的高度。减少渲染开支。

代码解读 以billboard为例。1.126版本
1.Billboard.js
1. Billboard是不能直接实例化的,是通过BillboardCollection.add()。来实例化Billboard,从而添加Billboard。而关于贴地的函数是写在Billboard.js上。我们打开其源码会发现。有个_updateClamping 的私有函数。
function Billboard(options, billboardCollection) {
  options = defaultValue(options, defaultValue.EMPTY_OBJECT);
........忽略中间代码

  this._updateClamping();  //关键代码
}
2.this._updateClamping() 函数
Billboard.prototype._updateClamping = function () {
  Billboard._updateClamping(this._billboardCollection, this);
};

const scratchCartographic = new Cartographic();
Billboard._updateClamping = function (collection, owner) {
  const scene = collection._scene;
  if (!defined(scene)) {
    //>>includeStart('debug', pragmas.debug);
    if (owner._heightReference !== HeightReference.NONE) {
      throw new DeveloperError(
        "Height reference is not supported without a scene.",
      );
    }
    //>>includeEnd('debug');
    return;
  }

  const ellipsoid = defaultValue(scene.ellipsoid, Ellipsoid.default);

  const mode = scene.frameState.mode;

  const modeChanged = mode !== owner._mode;
  owner._mode = mode;

  if (
    (owner._heightReference === HeightReference.NONE || modeChanged) &&
    defined(owner._removeCallbackFunc)
  ) {
    owner._removeCallbackFunc();
    owner._removeCallbackFunc = undefined;
    owner._clampedPosition = undefined;
  }

  if (
    owner._heightReference === HeightReference.NONE ||
    !defined(owner._position)
  ) {
    return;
  }

  if (defined(owner._removeCallbackFunc)) {
    owner._removeCallbackFunc();
  }

  const position = ellipsoid.cartesianToCartographic(owner._position);
  if (!defined(position)) {
    owner._actualClampedPosition = undefined;
    return;
  }

  function updateFunction(clampedPosition) {
    const updatedClampedPosition = ellipsoid.cartographicToCartesian(
      clampedPosition,
      owner._clampedPosition,
    );

    if (isHeightReferenceRelative(owner._heightReference)) {
      if (owner._mode === SceneMode.SCENE3D) {
        clampedPosition.height += position.height;
        ellipsoid.cartographicToCartesian(
          clampedPosition,
          updatedClampedPosition,
        );
      } else {
        updatedClampedPosition.x += position.height;
      }
    }

    owner._clampedPosition = updatedClampedPosition;
  }

  owner._removeCallbackFunc = scene.updateHeight(  
    position,
    updateFunction,
    owner._heightReference,
  );

  Cartographic.clone(position, scratchCartographic);
  const height = scene.getHeight(position, owner._heightReference);
  if (defined(height)) {
    scratchCartographic.height = height;
  }

  updateFunction(scratchCartographic); 
};

我们一步步解读,每段代码的作用。

1.判断HeightReferenceRelative

前面是判断scene是否存在。其中判断HeightReferenceRelative是否为贴地,若为null则退出该函数。

2.往下阅读,我们会发现有段代码是owner._removeCallbackFunc() 。这段代码的作用是移除该类的更新高度函数(即移除后面代码中的updateFunction函数)。使其之前的更新高度的回调函数不再被调用,为后续传递新的回调函数做准备,也避免重复调用。

_removeCallbackFunc是如何而来的后续会讲到,会涉及到前面提到的3个js文件。

3.创建updateFunction函数(关键函数)。它是一个回调函数。该函数只有在该点被新的瓦片范围包含的时候才会被调用,参数返回的是该点最新的高度值。

4.调用scene.getHeight()获取当前地形的高度。然后调用updateFunction()把最新高度传递过去。以便初始化的时候就调用。

billboard只是记录最新的高度。此时最新的高度还未在地图上更新,更新高度是在billboardCollection.js上统一为billboard进行更新。

2.scene.js

billboard.js中的_updateClamping() 发现有一段是调用了scene.updateHeight() 函数。该函数起到了重要作用。后续我们需要修改也只能从该函数上修改。

updateHeight()
Scene.prototype.updateHeight = function (
  cartographic,
  callback,
  heightReference,
) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.func("callback", callback);
  //>>includeEnd('debug');
//第一段
  const callbackWrapper = () => {
    Cartographic.clone(cartographic, updateHeightScratchCartographic);

    const height = this.getHeight(cartographic, heightReference);
    if (defined(height)) {
      updateHeightScratchCartographic.height = height;
      callback(updateHeightScratchCartographic);      
    }
  };
//第二段

  const ignore3dTiles =
    heightReference === HeightReference.CLAMP_TO_TERRAIN ||
    heightReference === HeightReference.RELATIVE_TO_TERRAIN;

  const ignoreTerrain =
    heightReference === HeightReference.CLAMP_TO_3D_TILE ||
    heightReference === HeightReference.RELATIVE_TO_3D_TILE;

//第三段
  let terrainRemoveCallback;
  if (!ignoreTerrain && defined(this.globe)) {
    terrainRemoveCallback = this.globe._surface.updateHeight(   
      cartographic,
      callbackWrapper,
    );
  }
//第四段
  let tilesetRemoveCallbacks = {};
  const ellipsoid = this._ellipsoid;
  const createPrimitiveEventListener = (primitive) => {
    if (
      ignore3dTiles ||
      primitive.isDestroyed() ||
      !primitive.isCesium3DTileset
    ) {
      return;
    }

    const tilesetRemoveCallback = primitive.updateHeight(
      cartographic,
      callbackWrapper,
      ellipsoid,
    );
    tilesetRemoveCallbacks[primitive.id] = tilesetRemoveCallback;
  };

  if (!ignore3dTiles) {
    const length = this.primitives.length;
    for (let i = 0; i < length; ++i) {
      const primitive = this.primitives.get(i);
      createPrimitiveEventListener(primitive);
    }
  }


  const removeAddedListener = this.primitives.primitiveAdded.addEventListener(
    createPrimitiveEventListener,
  );
  const removeRemovedListener =
    this.primitives.primitiveRemoved.addEventListener((primitive) => {
      if (primitive.isDestroyed() || !primitive.isCesium3DTileset) {
        return;
      }
      if (defined(tilesetRemoveCallbacks[primitive.id])) {
        tilesetRemoveCallbacks[primitive.id]();
      }
      delete tilesetRemoveCallbacks[primitive.id];
    });

//第五段
  const removeCallback = () => {
    terrainRemoveCallback = terrainRemoveCallback && terrainRemoveCallback();
    Object.values(tilesetRemoveCallbacks).forEach((tilesetRemoveCallback) =>
      tilesetRemoveCallback(),
    );
    tilesetRemoveCallbacks = {};
    removeAddedListener();
    removeRemovedListener();
  };

  return removeCallback;
};

这段代码可以分为5段。(注释上有写)

第1段:创建一个callbackWrapper 的函数。函数里面就是调用我们前面billboard中的updateHeight函数的回调。并把当前的高度传递过去。但此时callbackWrapper函数还未被调用。而该callbackWrapper也是一个回调函数来的。是传递给globe._surface 的updateHeight函数。后面会讲到。

第2段:判断是3dtiles还是地形。

第3段:根据第2段的判断去执行,这里我们是贴地类型即CLAMP_TO_TERRAIN

因此会执行该函数。

第4段:忽略。。。(我们只看与之相关的代码)

第5段:创建一个removeCallback函数。并返回。我们从而Billboard.jsowner._removeCallbackFunc 就是这个removeCallback函数。

需要注意的是该removeCallback函数中terrainRemoveCallback是由前面的globe.jsupdateHeight所返回的。就是前面所说用来移除该点的更新回调。

说到这里可能有些人会迷糊了。但通过我们的解析会发现,关键的代码就在callbackWrapper这个回调函数中。是谁执行了它,只有执行了该函数才能更新billboard

callbackWrapper是作为参数传递给globe._surface的updateHeight函数。接下来继续看一步步去解析。通过看globe.js 会发现_surface的属性其实是QuadtreePrimitive类。这里就忽略了globe.js的解读。我们直接看QuadtreePrimitive

 terrainRemoveCallback = this.globe._surface.updateHeight(   
      cartographic,
      callbackWrapper,
    );
3.QuadtreePrimitive.js

这里关键的函数有: 

1.updateHeight(cartographic, callback)
2.render()
3.endFrame()
4.selectTilesForRendering(primitive, frameState)
5.updateHeights(primitive, frameState)

大家可自行去了解。

我们从updateHeight开始解读,毕竟在scene.js的时候就有调用它

updateHeight()
QuadtreePrimitive.prototype.updateHeight = function (cartographic, callback) {
  const primitive = this;
  const object = {
    positionOnEllipsoidSurface: undefined,
    positionCartographic: cartographic,
    level: -1,
    callback: callback,
  };

  object.removeFunc = function () {   
    const addedCallbacks = primitive._addHeightCallbacks;
    const length = addedCallbacks.length;
    for (let i = 0; i < length; ++i) {
      if (addedCallbacks[i] === object) {
        addedCallbacks.splice(i, 1);
        break;
      }
    }
    primitive._removeHeightCallbacks.push(object);   
    if (object.callback) {
      object.callback = undefined;
    }
  };

  primitive._addHeightCallbacks.push(object);
  return object.removeFunc;
};

不难发现 callback中的值其实就是前面所提到的callbackWrapper函数。用来更新点位的高度

cartographic则是billboard.js所在的点位的经纬度。

而该函数所返回的是billboard.jsremoveCallbck函数。

这里需要注意,removeFunc的函数里面的 addedCallbacks里面数量就是需要更新高度的点位数量。

而当移除了的,也会添加在_removeHeightCallbacks待移除数组中。

最后将object 放入到_addHeightCallbacks数组中。这个数组将会在瓦片更新的时候执行,如果符合条件则会执行里面的callback。从而更新点位的高度。

selectTilesForRendering(primitive, frameState)
function selectTilesForRendering(primitive, frameState) {  
 
  //忽略。。。。。

  const customDataAdded = primitive._addHeightCallbacks;
  const customDataRemoved = primitive._removeHeightCallbacks;
  const frameNumber = frameState.frameNumber;

  let len;
 
  if (customDataAdded.length > 0 || customDataRemoved.length > 0) {
    for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
      tile = levelZeroTiles[i];
      tile._updateCustomData(frameNumber, customDataAdded, customDataRemoved);   
    }

    customDataAdded.length = 0;
    customDataRemoved.length = 0;
  }

//  。。。。忽略

 // Traverse in depth-first, near-to-far order.
  for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
    tile = levelZeroTiles[i];
    primitive._tileReplacementQueue.markTileRendered(tile);
    if (!tile.renderable) {
      queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
      ++debug.tilesWaitingForChildren;
    } else {
      visitIfVisible(
        primitive,
        tile,
        tileProvider,
        frameState,
        occluders,
        false,
        rootTraversalDetails[i],
      );
    }
  }
}

这里我们只需要看这段核心代码。

这里的_addHeightCallbacks_removeHeightCallbacks 就是前面所提到的数组。

这些数组会传到_updateCustomData这个函数中。而这个tile 其实是QuadtreeTile.js 类。 在后面我们会继续讲到。 并且它的调用是在updateHeights被调用里面。

在代码最下面会有visitIfVisible函数的调用。这里会有会存_tileToUpdateHeights的一个数组。这个数组里面是存放tile的。由于篇幅这里就不细致讲解。

updateHeights()
function updateHeights(primitive, frameState) {
  if (!defined(primitive.tileProvider.tilingScheme)) {
    return;
  }

  const tryNextFrame = scratchArray;
  tryNextFrame.length = 0;
  const tilesToUpdateHeights = primitive._tileToUpdateHeights;

  const startTime = getTimestamp();
  const timeSlice = primitive._updateHeightsTimeSlice;
  const endTime = startTime + timeSlice;

  const mode = frameState.mode;
  const projection = frameState.mapProjection;
  const ellipsoid = primitive.tileProvider.tilingScheme.ellipsoid;
  let i;

  while (tilesToUpdateHeights.length > 0) {    
    const tile = tilesToUpdateHeights[0];
   //。。。忽略
    const customData = tile.customData;
    const customDataLength = customData.length;

    let timeSliceMax = false;
    for (i = primitive._lastTileIndex; i < customDataLength; ++i) {
      const data = customData[i];

    // 。。。忽略

        const position = tile.data.pick(
          scratchRay,
          mode,
          projection,
          false,
          scratchPosition,
        );
        
        if (defined(position)) {
          if (defined(data.callback)) {
            data.callback(position); 
          }
          data.level = tile.level;
        }
      }

      if (getTimestamp() >= endTime) {
        timeSliceMax = true;
        break;
      }
    }

    if (timeSliceMax) {
      primitive._lastTileIndex = i;
      break;
    } else {
      primitive._lastTileIndex = 0;
      tilesToUpdateHeights.shift();
    }
  }
  for (i = 0; i < tryNextFrame.length; i++) {
    tilesToUpdateHeights.push(tryNextFrame[i]);
  }
}

到了最关键的函数了,一层层分析下来终于到了最后执行的函数。

这段函数是会被一直执行。在下面的endFrame函数中。你可以把其当作requestAnimationFrame一直执行就可以。

首先,我们看到

 const tilesToUpdateHeights = primitive._tileToUpdateHeights;

前面我们提到这里是存放tile的,这个tile就是前面提到的QuadtreeTile类。

我们会发现_tileToUpdateHeights在循环中。它会获取tile.customData里面的数据。

customData数组其实就是我们前面所提到的_updateCustomData函数里面的数据,而customData里面的数据就是我们前面所提到的updateHeight()函数里面的obj。

当有新的瓦片进来的时候,tilesToUpdateHeights就会增加。而tilesToUpdateHeights里面就是新增的tile。该tile中的custom数组里的数据就是符合在瓦片内的点,就是这些点的更新高度的回调。

我们注意到下面的callback。其实就是我们上面所存的callback更新点位高度的回调函数了。至此点位高度更新完毕,使billboard只要一有瓦片符合都会更新其高度位置。

data.callback(position); 
endFrame()
QuadtreePrimitive.prototype.endFrame = function (frameState) {    
  const passes = frameState.passes;
  if (!passes.render || frameState.mode === SceneMode.MORPHING) {
    // Only process the load queue for a single pass.
    // Don't process the load queue or update heights during the morph flights.
    return;
  }

  // Load/create resources for terrain and imagery. Prepare texture re-projections for the next frame.
  processTileLoadQueue(this, frameState);
  updateHeights(this, frameState);
  updateTileLoadProgress(this, frameState);
};

这段函数就是一直会被调用。一直执行updateHeight函数。

render()
QuadtreePrimitive.prototype.render = function (frameState) {
  const passes = frameState.passes;
  const tileProvider = this._tileProvider;

  if (passes.render) {
    tileProvider.beginUpdate(frameState);

    selectTilesForRendering(this, frameState);   //这里一直调用。
    createRenderCommandsForSelectedTiles(this, frameState);

    tileProvider.endUpdate(frameState);
  }

  if (passes.pick && this._tilesToRender.length > 0) {
    tileProvider.updateForPick(frameState);
  }
};

这段函数也是渲染函数 会一直调用我们前面所说selectTilesForRendering函数。 当然是render先执行。然后endFrame()才执行,它们之间是有个先后顺序的

4.QuadtreeTile.js

_updateCustomData()

QuadtreeTile.prototype._updateCustomData = function (
  frameNumber,
  added,
  removed,
) {
  let customData = this.customData;

  let i;
  let data;
  let rectangle;

  if (defined(added) && defined(removed)) {
    customData = customData.filter(function (value) {
      return removed.indexOf(value) === -1;
    });
    this._customData = customData;

    rectangle = this._rectangle;  
    for (i = 0; i < added.length; ++i) {
      data = added[i];
      if (Rectangle.contains(rectangle, data.positionCartographic)) {
        customData.push(data);   
      }
    }

    this._frameUpdated = frameNumber;
  } else {
    // interior or leaf tile, update from parent
    const parent = this._parent;
    if (defined(parent) && this._frameUpdated !== parent._frameUpdated) {
      customData.length = 0;

      rectangle = this._rectangle;
      const parentCustomData = parent.customData;
      for (i = 0; i < parentCustomData.length; ++i) {
        data = parentCustomData[i];
        if (Rectangle.contains(rectangle, data.positionCartographic)) {
          customData.push(data);
        }
      }

      this._frameUpdated = parent._frameUpdated;
    }
  }
};

这个类就是瓦片tile。

这里我们需要注意的是,该函数的调用是在selectTilesForRendering函数中被调用了

其中customData就是前面提到的selectTilesForRendering函数中存储obj的数组

added就是前面所提到的_addHeightCallbacks,这里需要注意的是只有当点位在矩形范围内才会存入至customData中。

  rectangle = this._rectangle;  
    for (i = 0; i < added.length; ++i) {
      data = added[i];
      if (Rectangle.contains(rectangle, data.positionCartographic)) {
        customData.push(data);   
      }
    }
应用场景

当我们需要瓦片更新就调用某些方法的业务场景的时候,不妨可以使用scene.updateHeight函数。这样也避免了修改源码。

传递相应的回调数据,这样只要当瓦片更新并且点位是在该瓦片内的,就会执行回调。

可以运用在pointPrimitive身上使用贴地效果。相比entity ,加载上千个点的时候primitive的性能会更佳。

至此测cesium的贴地的全过程讲解完毕,耗时3天,如有疏漏恳请大家纠正补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值