-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathheight-map-builder.ts
More file actions
155 lines (138 loc) · 5.72 KB
/
height-map-builder.ts
File metadata and controls
155 lines (138 loc) · 5.72 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
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {Device, Framebuffer} from '@luma.gl/core';
import {
joinLayerBounds,
getRenderBounds,
makeViewport,
getMercatorReferenceViewport,
lngLatToMercatorCommon,
Bounds
} from '../utils/projection-utils';
import {createRenderTarget} from './utils';
import type {Viewport, Layer} from '@deck.gl/core';
const MAP_MAX_SIZE = 2048;
/**
* Manages the lifecycle of the height map (a framebuffer that encodes elevation).
* One instance of height map is is shared across all layers. It is updated when the viewport changes
* or when some terrain source layer's data changes.
* During the draw call of any terrainDrawMode:offset layers,
* the vertex shader reads from this framebuffer to retrieve its z offset.
*/
export class HeightMapBuilder {
/** Viewport used to draw into the texture */
renderViewport: Viewport | null = null;
/** Bounds of the height map texture, in cartesian space */
bounds: Bounds | null = null;
protected fbo?: Framebuffer;
protected device: Device;
/** Last rendered layers */
private layers: Layer[] = [];
/** Last layer.getBounds() */
private layersBounds: ([number[], number[]] | null)[] = [];
/** The union of layersBounds in cartesian space */
private layersBoundsCommon: Bounds | null = null;
private lastViewport: Viewport | null = null;
static isSupported(device: Device): boolean {
return device.isTextureFormatRenderable('rgba32float');
}
constructor(device: Device) {
this.device = device;
}
/** Returns the height map framebuffer for read/write access.
* Returns null when the texture is invalid.
*/
getRenderFramebuffer(): Framebuffer | null {
if (!this.renderViewport) {
return null;
}
if (!this.fbo) {
this.fbo = createRenderTarget(this.device, {id: 'height-map', float: true});
}
return this.fbo;
}
/** Called every render cycle to check if the framebuffer needs update */
shouldUpdate({layers, viewport}: {layers: Layer[]; viewport: Viewport}): boolean {
const layersChanged =
layers.length !== this.layers.length ||
layers.some(
(layer, i) =>
// Layer instance is updated
// Layer props might have changed
// Undetermined props could have an effect on the output geometry of a terrain source,
// for example getElevation+updateTriggers, elevationScale, modelMatrix
layer !== this.layers[i] ||
// Some prop is in transition
layer.props.transitions ||
// Layer's geometry bounds have changed
layer.getBounds() !== this.layersBounds[i]
);
if (layersChanged) {
// Recalculate cached bounds.
// Use a Mercator reference viewport so layer bounds live in ABSOLUTE
// Mercator common space — same rationale as terrain-cover.ts. On
// GlobeView, `viewport.projectPosition` would return 3D sphere cartesian
// coords that can't be compared against screen-space render bounds.
this.layers = layers;
this.layersBounds = layers.map(layer => layer.getBounds());
this.layersBoundsCommon = joinLayerBounds(layers, getMercatorReferenceViewport(viewport));
}
const viewportChanged = !this.lastViewport || !viewport.equals(this.lastViewport);
if (!this.layersBoundsCommon) {
this.renderViewport = null;
} else if (layersChanged || viewportChanged) {
// getRenderBounds intersects layer bounds with viewport bounds. On globe,
// viewport bounds project to sphere cartesian and won't intersect the
// Mercator layer bounds meaningfully — use the full layer bounds instead.
const isGlobe = Boolean(
(viewport as {resolution?: number}).resolution &&
(viewport as {resolution?: number}).resolution! > 0
);
const bounds = isGlobe
? this.layersBoundsCommon
: getRenderBounds(this.layersBoundsCommon, viewport);
if (bounds[2] <= bounds[0] || bounds[3] <= bounds[1]) {
this.renderViewport = null;
return false;
}
this.bounds = bounds;
this.lastViewport = viewport;
const scale = viewport.scale;
const pixelWidth = (bounds[2] - bounds[0]) * scale;
const pixelHeight = (bounds[3] - bounds[1]) * scale;
// Center for the render viewport must be expressed in Mercator common so
// makeViewport (which unprojects through a WebMercatorViewport for
// geospatial inputs) gets a valid lng/lat back. `viewport.center` on
// GlobeView is 3D sphere cartesian and would unproject bogusly.
const centerMerc = viewport.isGeospatial
? lngLatToMercatorCommon([
(viewport as {longitude?: number}).longitude ?? 0,
(viewport as {latitude?: number}).latitude ?? 0
])
: [viewport.center[0], viewport.center[1]];
this.renderViewport =
pixelWidth > 0 || pixelHeight > 0
? makeViewport({
// It's not important whether the geometry is visible in this viewport, because
// vertices will not use the standard project_to_clipspace in the DRAW_TO_HEIGHT_MAP shader
// However the viewport must have the same center and zoom as the screen viewport
// So that projection uniforms used for calculating z are the same
bounds: [centerMerc[0] - 1, centerMerc[1] - 1, centerMerc[0] + 1, centerMerc[1] + 1],
zoom: viewport.zoom,
width: Math.min(pixelWidth, MAP_MAX_SIZE),
height: Math.min(pixelHeight, MAP_MAX_SIZE),
viewport
})
: null;
return true;
}
return false;
}
delete() {
if (this.fbo) {
this.fbo.colorAttachments[0].delete();
this.fbo.delete();
}
}
}