篇幅有点长。。请大家耐心观看。
附上经过处理后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.js中owner._removeCallbackFunc 就是这个removeCallback函数。
需要注意的是该removeCallback函数中terrainRemoveCallback是由前面的globe.js 的updateHeight所返回的。就是前面所说用来移除该点的更新回调。
说到这里可能有些人会迷糊了。但通过我们的解析会发现,关键的代码就在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.js 中 removeCallbck函数。
这里需要注意,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天,如有疏漏恳请大家纠正补充。