Skip to content

Commit

Permalink
perf(LabelLayer): add automatic label filtering to reduce rendering c…
Browse files Browse the repository at this point in the history
…alls.

Filter all labels that will never be visible
  • Loading branch information
gchoqueux committed May 10, 2023
1 parent aff7964 commit e7dde10
Show file tree
Hide file tree
Showing 11 changed files with 73 additions and 10 deletions.
2 changes: 1 addition & 1 deletion examples/vector_tile_3d_mesh.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
source: mapSource,
effect_type: itowns.colorLayerEffects.removeLightColor,
effect_parameter: 2.5,
addLabelLayer: true,
addLabelLayer: { performance: true },
style: new itowns.Style({
text: {
color: '#000000',
Expand Down
2 changes: 1 addition & 1 deletion examples/vector_tile_raster_2d.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@

var mvtLayer = new itowns.ColorLayer('MVT', {
source: mvtSource,
addLabelLayer: true,
addLabelLayer: { performance: true },
});

view.addLayer(mvtLayer).then(menuGlobe.addLayerGUI.bind(menuGlobe));
Expand Down
2 changes: 1 addition & 1 deletion examples/vector_tile_raster_3d.html
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
source: mvtSource,
effect_type: itowns.colorLayerEffects.removeLightColor,
effect_parameter: 2.5,
addLabelLayer: true,
addLabelLayer: { performance: true },
});

view.addLayer(mvtLayer);
Expand Down
2 changes: 1 addition & 1 deletion examples/view_3d_map.html
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
filter: (layer) => !layer['source-layer'].includes('oro_')
&& !layer['source-layer'].includes('parcellaire'),
}),
addLabelLayer: true,
addLabelLayer: { performance: true },
}),
{ cursor: '+' },
);
Expand Down
2 changes: 1 addition & 1 deletion examples/view_3d_mns_map.html
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
filter: (layer) => !layer['source-layer'].includes('oro_')
&& !layer['source-layer'].includes('parcellaire'),
}),
addLabelLayer: true,
addLabelLayer: { performance: true },
}),
{ cursor: '+' },
);
Expand Down
2 changes: 1 addition & 1 deletion examples/widgets_minimap.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
filter: (layer) => !layer['source-layer'].includes('oro_')
&& !layer['source-layer'].includes('parcellaire'),
}),
addLabelLayer: true,
addLabelLayer: { performance: true },
});

// Create a minimap.
Expand Down
1 change: 1 addition & 0 deletions src/Core/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ function _preprocessLayer(view, layer, parentLayer) {
source,
style: layer.style,
zoom: layer.zoom,
performance: layer.addLabelLayer.performance,
crs: source.crs,
visible: layer.visible,
margin: 15,
Expand Down
57 changes: 57 additions & 0 deletions src/Layer/LabelLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@ import Extent from 'Core/Geographic/Extent';
import Label from 'Core/Label';
import { FEATURE_TYPES } from 'Core/Feature';
import { readExpression } from 'Core/Style';
import { ScreenGrid } from 'Renderer/Label2DRenderer';

const coord = new Coordinates('EPSG:4326', 0, 0, 0);

const _extent = new Extent('EPSG:4326', 0, 0, 0, 0);

const nodeDimensions = new THREE.Vector2();
const westNorthNode = new THREE.Vector2();
const labelPosition = new THREE.Vector2();

/**
* DomNode is a node in the tree data structure of labels divs.
*
Expand Down Expand Up @@ -138,6 +143,7 @@ class LabelsNode extends THREE.Group {
* internally for optimisation.
*/
class LabelLayer extends GeometryLayer {
#filterGrid = new ScreenGrid();
/**
* @constructor
* @extends Layer
Expand All @@ -150,6 +156,11 @@ class LabelLayer extends GeometryLayer {
* contains three elements `name, protocol, extent`, these elements will be
* available using `layer.name` or something else depending on the property
* name.
* @param {boolean} [config.performance=true] - remove labels that have no chance of being visible.
* if the `config.performance` is set to true then the performance is improved
* proportional to the amount of unnecessary labels that are removed.
* Indeed, even in the best case, labels will never be displayed. By example, if there's many labels.
* We advise you to not use this option if your data is optimized.
* @param {domElement|function} config.domElement - An HTML domElement.
* If set, all `Label` displayed within the current instance `LabelLayer`
* will be this domElement.
Expand All @@ -171,6 +182,7 @@ class LabelLayer extends GeometryLayer {
this.domElement.dom.id = `itowns-label-${this.id}`;
this.buildExtent = true;
this.crs = config.source.crs;
this.performance = config.performance || true;

this.labelDomelement = domElement;

Expand Down Expand Up @@ -286,6 +298,9 @@ class LabelLayer extends GeometryLayer {
preUpdate(context, sources) {
if (sources.has(this.parent)) {
this.object3d.clear();
this.#filterGrid.width = this.parent.maxScreenSizeNode * 0.5;
this.#filterGrid.height = this.parent.maxScreenSizeNode * 0.5;
this.#filterGrid.resize();
}
}

Expand Down Expand Up @@ -313,6 +328,44 @@ class LabelLayer extends GeometryLayer {
return object.children.every(c => c.layerUpdateState && c.layerUpdateState[this.id]?.hasFinished());
}

// Remove all labels invisible with pre-culling with screen grid
// We use the screen grid with maximum size of node on screen
#removeCulledLabels(node) {
// copy labels array
const labels = node.children.slice();

// reset filter
this.#filterGrid.reset();

// sort labels by order
labels.sort((a, b) => b.order - a.order);

labels.forEach((label) => {
// get node dimensions
node.nodeParent.extent.planarDimensions(nodeDimensions);
coord.crs = node.nodeParent.extent.crs;

// get west/north node coordinates
coord.setFromValues(node.nodeParent.extent.west, node.nodeParent.extent.north, 0).toVector3(westNorthNode);

// get label position
coord.copy(label.coordinates).as(node.nodeParent.extent.crs, coord).toVector3(labelPosition);

// transform label position to local node system
labelPosition.sub(westNorthNode);
labelPosition.y += nodeDimensions.y;
labelPosition.divide(nodeDimensions).multiplyScalar(this.#filterGrid.width);

// update the projected position to transform to local filter grid sytem
label.updateProjectedPosition(labelPosition.x, labelPosition.y);

// use screen grid to remove all culled labels
if (!this.#filterGrid.insert(label)) {
node.removeLabel(label);
}
});
}

update(context, layer, node, parent) {
if (!parent && node.link[layer.id]) {
// if node has been removed dispose three.js resource
Expand Down Expand Up @@ -406,6 +459,10 @@ class LabelLayer extends GeometryLayer {
if (labelsNode.needsAltitude && node.material.getElevationLayer()) {
node.material.getElevationLayer().addEventListener('rasterElevationLevelChanged', () => { labelsNode.needsUpdate = true; });
}

if (this.performance) {
this.#removeCulledLabels(labelsNode);
}
}

node.layerUpdateState[this.id].noMoreUpdatePossible();
Expand Down
9 changes: 6 additions & 3 deletions src/Layer/Layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,6 @@ import Cache from 'Core/Scheduler/Cache';
* @property {Promise} whenReady - this promise is resolved when the layer is added and all initializations are done.
* This promise is resolved with this layer.
* This promise is returned by [View#addLayer]{@link View}.
* @property {boolean} [addLabelLayer=false] - Used to tell if this layer has
* labels to display from its data. For example, it needs to be set to `true`
* for a layer with vector tiles. If it's `true` a new `LabelLayer` is added and attached to this `Layer`.
* @property {object} [zoom] - This property is used only the layer is attached to [TiledGeometryLayer]{@link TiledGeometryLayer}.
* By example,
* The layer checks the tile zoom level to determine if the layer is visible in this tile.
Expand Down Expand Up @@ -56,6 +53,12 @@ class Layer extends THREE.EventDispatcher {
* the layer doesn't need Source (like debug Layer or procedural layer).
* @param {number} [config.cacheLifeTime=Infinity] - set life time value in cache.
* This value is used for [Cache]{@link Cache} expiration mechanism.
* @param {(boolean|Object)} [config.addLabelLayer=false] - Used to tell if this layer has
* labels to display from its data. For example, it needs to be set to `true`
* for a layer with vector tiles. If it's `true` a new `LabelLayer` is added and attached to this `Layer`.
* You can also configure it with [LabelLayer]{@link LabelLayer} options described below such as: `addLabelLayer: { performance: true }`.
* @param {boolean} [config.addLabelLayer.performance=false] - In case label layer adding, so remove labels that have no chance of being visible.
* Indeed, even in the best case, labels will never be displayed. By example, if there's many labels.
*
* @example
* // Add and create a new Layer
Expand Down
2 changes: 2 additions & 0 deletions src/Layer/TiledGeometryLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ class TiledGeometryLayer extends GeometryLayer {
this.object3d.add(...level0s);
this.object3d.updateMatrixWorld();
}));

this.maxScreenSizeNode = this.sseSubdivisionThreshold * (SIZE_DIAGONAL_TEXTURE * 2);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Renderer/Label2DRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function isIntersectedOrOverlaped(a, b) {
const frustum = new THREE.Frustum();

// A grid to manage labels on the screen.
class ScreenGrid {
export class ScreenGrid {
constructor(x = 12, y = 10, width, height) {
this.x = x;
this.y = y;
Expand Down

0 comments on commit e7dde10

Please sign in to comment.