-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathterrain-cover.ts
More file actions
265 lines (236 loc) · 8.28 KB
/
terrain-cover.ts
File metadata and controls
265 lines (236 loc) · 8.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {Framebuffer} from '@luma.gl/core';
import type {Layer, Viewport} from '@deck.gl/core';
import {createRenderTarget} from './utils';
import {
getMercatorReferenceViewport,
joinLayerBounds,
lngLatToMercatorCommon,
makeViewport,
getRenderBounds,
Bounds
} from '../utils/projection-utils';
type TileHeader = {
boundingBox: [min: number[], max: number[]];
};
/**
* Manages the lifecycle of the terrain cover (draped textures over a terrain mesh).
* One terrain cover is created for each unique terrain layer (primitive layer with operation:terrain).
* It is updated when the terrain source layer's mesh changes or when any of the terrainDrawMode:drape
* layers requires redraw.
* During the draw call of a terrain layer, the drape texture is overlaid on top of the layer's own color.
*/
export class TerrainCover {
isDirty: boolean = true;
/** The terrain layer that this instance belongs to */
targetLayer: Layer;
/** Viewport used to draw into the texture */
renderViewport: Viewport | null = null;
/** Bounds of the terrain cover texture, in cartesian space */
bounds: Bounds | null = null;
private fbo?: Framebuffer;
private pickingFbo?: Framebuffer;
private layers: string[] = [];
private tile: TileHeader | null;
/** Cached version of targetLayer.getBounds() */
private targetBounds: [number[], number[]] | null = null;
/** targetBounds in cartesian space */
private targetBoundsCommon: Bounds | null = null;
constructor(targetLayer: Layer) {
this.targetLayer = targetLayer;
this.tile = getTile(targetLayer);
}
get id() {
return this.targetLayer.id;
}
/** returns true if the target layer is still in use (i.e. not finalized) */
get isActive(): boolean {
return Boolean(this.targetLayer.getCurrentLayer());
}
shouldUpdate({
targetLayer,
viewport,
layers,
layerNeedsRedraw
}: {
targetLayer?: Layer;
viewport?: Viewport;
layers?: Layer[];
layerNeedsRedraw?: Record<string, boolean>;
}): boolean {
if (targetLayer) {
this.targetLayer = targetLayer;
}
const sizeChanged = viewport ? this._updateViewport(viewport) : false;
let layersChanged = layers ? this._updateLayers(layers) : false;
if (layerNeedsRedraw) {
for (const id of this.layers) {
if (layerNeedsRedraw[id]) {
layersChanged = true;
// console.log('layer needs redraw', id);
break;
}
}
}
return layersChanged || sizeChanged;
}
/** Compare layers with the last version. Only rerender if necessary. */
private _updateLayers(layers: Layer[]): boolean {
let needsRedraw = false;
layers = this.tile ? getIntersectingLayers(this.tile, layers) : layers;
if (layers.length !== this.layers.length) {
needsRedraw = true;
// console.log('layers count changed', this.layers.length, '>>', layers.length);
} else {
for (let i = 0; i < layers.length; i++) {
const id = layers[i].id;
if (id !== this.layers[i]) {
needsRedraw = true;
// console.log('layer added/removed', id);
break;
}
}
}
if (needsRedraw) {
this.layers = layers.map(layer => layer.id);
}
return needsRedraw;
}
/** Compare viewport and terrain bounds with the last version. Only rerender if necesary. */
// eslint-disable-next-line max-statements
private _updateViewport(viewport: Viewport): boolean {
const targetLayer = this.targetLayer;
let shouldRedraw = false;
// Bounds are computed in ABSOLUTE Mercator common space — NOT the live
// viewport's common space. The terrain cover FBO is rendered via a
// WebMercatorViewport regardless of the screen viewport, so UVs must also
// live in Mercator. This is what lets the same cover texture be sampled
// from MapView and GlobeView.
if (this.tile && 'boundingBox' in this.tile) {
if (!this.targetBounds) {
shouldRedraw = true;
this.targetBounds = this.tile.boundingBox;
const bottomLeftCommon = lngLatToMercatorCommon(this.targetBounds[0]);
const topRightCommon = lngLatToMercatorCommon(this.targetBounds[1]);
this.targetBoundsCommon = [
bottomLeftCommon[0],
bottomLeftCommon[1],
topRightCommon[0],
topRightCommon[1]
];
}
} else if (this.targetBounds !== targetLayer.getBounds()) {
// console.log('bounds changed', this.bounds, '>>', newBounds);
shouldRedraw = true;
this.targetBounds = targetLayer.getBounds();
// Non-tile terrain layer: project layer bounds through the Mercator
// reference so the cover is projection-invariant. joinLayerBounds uses
// layer.projectPosition() internally, which honors the layer's
// coordinateSystem (LNGLAT / CARTESIAN / METER_OFFSETS).
this.targetBoundsCommon = joinLayerBounds(
[targetLayer],
getMercatorReferenceViewport(viewport)
);
}
if (!this.targetBoundsCommon) {
return false;
}
const newZoom = Math.ceil(viewport.zoom + 0.5);
// If the terrain layer is bound to a tile, always render a texture that cover the whole tile.
// Otherwise, use the smaller of layer bounds and the viewport bounds.
if (this.tile) {
this.bounds = this.targetBoundsCommon;
} else {
const oldZoom = this.renderViewport?.zoom;
shouldRedraw = shouldRedraw || newZoom !== oldZoom;
// getRenderBounds intersects layer bounds (Mercator) with viewport bounds
// derived via viewport.projectPosition. On GlobeView that yields sphere
// cartesian coords, which would corrupt the intersection. Fall back to
// full layer bounds on non-Mercator geospatial viewports — resolution
// is reduced but output stays correct.
const isGlobe = Boolean(viewport.resolution && viewport.resolution > 0);
const newBounds = isGlobe
? this.targetBoundsCommon
: getRenderBounds(this.targetBoundsCommon, viewport);
const oldBounds = this.bounds;
shouldRedraw = shouldRedraw || !oldBounds || newBounds.some((x, i) => x !== oldBounds[i]);
this.bounds = newBounds;
}
if (shouldRedraw) {
this.renderViewport = makeViewport({
bounds: this.bounds,
zoom: newZoom,
viewport
});
}
return shouldRedraw;
}
getRenderFramebuffer(): Framebuffer | null {
if (!this.renderViewport || this.layers.length === 0) {
return null;
}
if (!this.fbo) {
this.fbo = createRenderTarget(this.targetLayer.context.device, {id: this.id});
}
return this.fbo;
}
getPickingFramebuffer(): Framebuffer | null {
if (!this.renderViewport || (this.layers.length === 0 && !this.targetLayer.props.pickable)) {
return null;
}
if (!this.pickingFbo) {
this.pickingFbo = createRenderTarget(this.targetLayer.context.device, {
id: `${this.id}-picking`,
interpolate: false
});
}
return this.pickingFbo;
}
filterLayers(layers: Layer[]) {
return layers.filter(({id}) => this.layers.includes(id));
}
delete() {
const {fbo, pickingFbo} = this;
if (fbo) {
fbo.colorAttachments[0].destroy();
fbo.destroy();
}
if (pickingFbo) {
pickingFbo.colorAttachments[0].destroy();
pickingFbo.destroy();
}
}
}
/**
* Remove layers that do not overlap with the current terrain cover.
* This implementation only has effect when a TileLayer is overlaid on top of a TileLayer
*/
function getIntersectingLayers(sourceTile: TileHeader, layers: Layer[]): Layer[] {
return layers.filter(layer => {
const tile = getTile(layer);
if (tile) {
return intersect(sourceTile.boundingBox, tile.boundingBox);
}
return true;
});
}
/** If layer is the descendent of a TileLayer, return the corresponding tile. */
function getTile(layer: Layer): TileHeader | null {
while (layer) {
// @ts-expect-error tile may not exist
const {tile} = layer.props;
if (tile) {
return tile;
}
layer = layer.parent as Layer;
}
return null;
}
function intersect(b1?: [number[], number[]], b2?: [number[], number[]]): boolean {
if (b1 && b2) {
return b1[0][0] < b2[1][0] && b2[0][0] < b1[1][0] && b1[0][1] < b2[1][1] && b2[0][1] < b1[1][1];
}
return false;
}