@deck.gl/extensions
Version:
Plug-and-play functionalities for deck.gl layers
130 lines (113 loc) • 4.53 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {Device, Framebuffer} from '@luma.gl/core';
import {joinLayerBounds, getRenderBounds, makeViewport, 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
this.layers = layers;
this.layersBounds = layers.map(layer => layer.getBounds());
this.layersBoundsCommon = joinLayerBounds(layers, viewport);
}
const viewportChanged = !this.lastViewport || !viewport.equals(this.lastViewport);
if (!this.layersBoundsCommon) {
this.renderViewport = null;
} else if (layersChanged || viewportChanged) {
const bounds = 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;
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: [
viewport.center[0] - 1,
viewport.center[1] - 1,
viewport.center[0] + 1,
viewport.center[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();
}
}
}