Skip to content

Commit

Permalink
Add MaskExtension (#6554)
Browse files Browse the repository at this point in the history
  • Loading branch information
felixpalmer authored Jan 26, 2022
1 parent 29373c6 commit cba5ddf
Show file tree
Hide file tree
Showing 35 changed files with 1,257 additions and 29 deletions.
137 changes: 137 additions & 0 deletions modules/core/src/effects/mask/mask-effect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
Texture2D
// , readPixelsToArray
} from '@luma.gl/core';
import {equals} from '@math.gl/core';
import MaskPass from '../../passes/mask-pass';
import Effect from '../../lib/effect';
import {OPERATION} from '../../lib/constants';
import {getMaskBounds, getMaskViewport} from './utils';

// Class to manage mask effect
export default class MaskEffect extends Effect {
constructor(props) {
super(props);
this.dummyMaskMap = null;
this.empty = true;
this.useInPicking = true;
}

preRender(gl, {layers, layerFilter, viewports, onViewportActive, views}) {
if (!this.dummyMaskMap) {
this.dummyMaskMap = new Texture2D(gl, {
width: 1,
height: 1
});
}

const maskLayers = layers.filter(l => l.props.operation === OPERATION.MASK);
if (maskLayers.length === 0) {
this.empty = true;
return;
}
this.empty = false;

if (!this.maskPass) {
// TODO - support multiple masks
this.maskPass = new MaskPass(gl, {id: 'default-mask'});
this.maskMap = this.maskPass.maskMap;
}

// When the mask layer is changed the LayerManager will create a new instance
const oldMaskLayers = this.maskLayers;
const maskChanged =
!oldMaskLayers ||
oldMaskLayers.length !== maskLayers.length ||
maskLayers.some((l, i) => l !== oldMaskLayers[i]);

// To do: support multiple views
const viewport = viewports[0];

if (maskChanged || !this.lastViewport || !viewport.equals(this.lastViewport)) {
// Update mask FBO
const {maskPass, maskMap} = this;
this.lastViewport = viewport;
this.maskLayers = maskLayers;

const bounds = getMaskBounds({layers: maskLayers, viewport});

// TODO if the mask layer's data has changed and the data bounds are clipped
// by the viewport, this condition will prevent the mask from rerendering
if (!equals(bounds, this.lastBounds)) {
this.lastBounds = bounds;

const maskViewport = getMaskViewport({
bounds,
viewport,
width: maskMap.width,
height: maskMap.height
});

this.maskBounds = maskViewport.getBounds();

maskPass.render({
layers,
layerFilter,
viewports: [maskViewport],
onViewportActive,
views,
moduleParameters: {
devicePixelRatio: 1
}
});

// // Debug show FBO contents on screen
// const color = readPixelsToArray(maskMap);
// let canvas = document.getElementById('fbo-canvas');
// if (!canvas) {
// canvas = document.createElement('canvas');
// canvas.id = 'fbo-canvas';
// canvas.width = maskMap.width;
// canvas.height = maskMap.height;
// canvas.style.zIndex = 100;
// canvas.style.position = 'absolute';
// canvas.style.right = 0;
// canvas.style.border = 'blue 1px solid';
// canvas.style.width = '256px';
// canvas.style.transform = 'scaleY(-1)';
// document.body.appendChild(canvas);
// }
// const ctx = canvas.getContext('2d');
// const imageData = ctx.createImageData(maskMap.width, maskMap.height);
// for (let i = 0; i < color.length; i += 4) {
// imageData.data[i + 0] = color[i + 0];
// imageData.data[i + 1] = color[i + 1];
// imageData.data[i + 2] = color[i + 2];
// imageData.data[i + 3] = color[i + 3];
// }
// ctx.putImageData(imageData, 0, 0);
}
}
}

getModuleParameters() {
return {
maskMap: this.empty ? this.dummyMaskMap : this.maskMap,
maskBounds: this.maskBounds
};
}

cleanup() {
if (this.dummyMaskMap) {
this.dummyMaskMap.delete();
this.dummyMaskMap = null;
}

if (this.maskPass) {
this.maskPass.delete();
this.maskPass = null;
this.maskMap = null;
}

this.lastBounds = null;
this.lastViewport = null;
this.maskBounds = null;
this.empty = true;
}
}
101 changes: 101 additions & 0 deletions modules/core/src/effects/mask/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import OrthographicView from '../../views/orthographic-view';
import WebMercatorViewport from '../../viewports/web-mercator-viewport';
import {fitBounds} from '@math.gl/web-mercator';
/*
* Compute the bounds of the mask in world space, such that it covers an
* area currently visible (extended by a buffer) or the area of the masking
* data, whichever is smaller
*/
export function getMaskBounds({layers, viewport}) {
// Join the bounds of layer data
let bounds = null;
for (const layer of layers) {
const subLayerBounds = layer.getBounds();
if (subLayerBounds) {
if (bounds) {
bounds[0] = Math.min(bounds[0], subLayerBounds[0][0]);
bounds[1] = Math.min(bounds[1], subLayerBounds[0][1]);
bounds[2] = Math.max(bounds[2], subLayerBounds[1][0]);
bounds[3] = Math.max(bounds[3], subLayerBounds[1][1]);
} else {
bounds = [
subLayerBounds[0][0],
subLayerBounds[0][1],
subLayerBounds[1][0],
subLayerBounds[1][1]
];
}
}
}
const viewportBounds = viewport.getBounds();
if (!bounds) {
return viewportBounds;
}

// Expand viewport bounds by 2X. Heurestically chosen to avoid masking
// errors when mask is partially out of view
const paddedBounds = _doubleBounds(viewportBounds);

// When bounds of the mask are smaller than the viewport bounds simply use
// mask bounds, so as to maximize resolution & avoid mask rerenders
if (
bounds[2] - bounds[0] < paddedBounds[2] - paddedBounds[0] ||
bounds[3] - bounds[1] < paddedBounds[3] - paddedBounds[1]
) {
return bounds;
}

// As viewport shrinks, to avoid pixelation along mask edges
// we need to reduce the bounds and only render the visible portion
// of the mask.
// We pad the viewport bounds to capture the section
// of the mask just outside the viewport to correctly maskByInstance.
// Intersect mask & padded viewport bounds
bounds[0] = Math.max(bounds[0], paddedBounds[0]);
bounds[1] = Math.max(bounds[1], paddedBounds[1]);
bounds[2] = Math.min(bounds[2], paddedBounds[2]);
bounds[3] = Math.min(bounds[3], paddedBounds[3]);
return bounds;
}

/*
* Compute viewport to render the mask into, covering the given bounds
*/
export function getMaskViewport({bounds, viewport, width, height}) {
if (viewport instanceof WebMercatorViewport) {
const {longitude, latitude, zoom} = fitBounds({
width,
height,
bounds: [
[bounds[0], bounds[1]],
[bounds[2], bounds[3]]
],
maxZoom: 20
});
return new WebMercatorViewport({longitude, latitude, zoom, width, height});
}

const center = [(bounds[0] + bounds[2]) / 2, (bounds[1] + bounds[3]) / 2, 0];
const scale = Math.min(20, width / (bounds[2] - bounds[0]), height / (bounds[3] - bounds[1]));

return new OrthographicView().makeViewport({
width,
height,
viewState: {
target: center,
zoom: Math.log2(scale)
}
});
}

function _doubleBounds(bounds) {
const size = {
x: bounds[2] - bounds[0],
y: bounds[3] - bounds[1]
};
const center = {
x: bounds[0] + 0.5 * size.x,
y: bounds[1] + 0.5 * size.y
};
return [center.x - size.x, center.y - size.y, center.x + size.x, center.y + size.y];
}
2 changes: 1 addition & 1 deletion modules/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import './lib/init';
import './shaderlib';

// Core Library
export {COORDINATE_SYSTEM, UNIT} from './lib/constants';
export {COORDINATE_SYSTEM, OPERATION, UNIT} from './lib/constants';

// Effects
export {default as LightingEffect} from './effects/lighting/lighting-effect';
Expand Down
4 changes: 3 additions & 1 deletion modules/core/src/lib/composite-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export default class CompositeLayer extends Layer {
modelMatrix,
extensions,
fetch,
operation,
_subLayerProps: overridingProps
} = this.props;
const newProps = {
Expand All @@ -169,7 +170,8 @@ export default class CompositeLayer extends Layer {
positionFormat,
modelMatrix,
extensions,
fetch
fetch,
operation
};

const overridingSublayerProps = overridingProps && overridingProps[sublayerProps.id];
Expand Down
8 changes: 8 additions & 0 deletions modules/core/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,11 @@ export const EVENTS = {
panmove: {handler: 'onDrag'},
panend: {handler: 'onDragEnd'}
} as const;

/**
* The rendering operation to perform with a layer, used in the `operation` prop
*/
export const OPERATION = {
DRAW: 'draw',
MASK: 'mask'
} as const;
11 changes: 9 additions & 2 deletions modules/core/src/lib/deck-picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ export default class DeckPicker {
depth = 1,
mode = 'query',
unproject3D,
onViewportActive
onViewportActive,
effects
}) {
layers = this._getPickable(layers);

Expand Down Expand Up @@ -188,6 +189,7 @@ export default class DeckPicker {
viewports,
onViewportActive,
deviceRect,
effects,
pass: `picking:${mode}`,
redrawReason: mode
});
Expand All @@ -208,6 +210,7 @@ export default class DeckPicker {
viewports,
onViewportActive,
deviceRect: {x: pickInfo.pickedX, y: pickInfo.pickedY, width: 1, height: 1},
effects,
pass: `picking:${mode}`,
redrawReason: 'pick-z',
pickZ: true
Expand Down Expand Up @@ -271,7 +274,8 @@ export default class DeckPicker {
height = 1,
mode = 'query',
maxObjects = null,
onViewportActive
onViewportActive,
effects
}) {
layers = this._getPickable(layers);

Expand Down Expand Up @@ -308,6 +312,7 @@ export default class DeckPicker {
viewports,
onViewportActive,
deviceRect,
effects,
pass: `picking:${mode}`,
redrawReason: mode
});
Expand Down Expand Up @@ -352,6 +357,7 @@ export default class DeckPicker {
viewports,
onViewportActive,
deviceRect,
effects,
pass,
redrawReason,
pickZ
Expand All @@ -366,6 +372,7 @@ export default class DeckPicker {
onViewportActive,
pickingFBO,
deviceRect,
effects,
pass,
redrawReason,
pickZ
Expand Down
1 change: 1 addition & 0 deletions modules/core/src/lib/deck.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ export default class Deck {
views: this.viewManager.getViews(),
viewports: this.getViewports(opts),
onViewportActive: this.layerManager.activateViewport,
effects: this.effectManager.getEffects(),
...opts
});

Expand Down
18 changes: 9 additions & 9 deletions modules/core/src/lib/effect-manager.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {deepEqual} from '../utils/deep-equal';
import {default as LightingEffect} from '../effects/lighting/lighting-effect';
import LightingEffect from '../effects/lighting/lighting-effect';
import MaskEffect from '../effects/mask/mask-effect';
import type Effect from './effect';

const DEFAULT_LIGHTING_EFFECT = new LightingEffect();
const DEFAULT_MASK_EFFECT = new MaskEffect();

export default class EffectManager {
effects: Effect[];
Expand Down Expand Up @@ -45,7 +47,12 @@ export default class EffectManager {
setEffects(effects = []) {
this.cleanup();
this.effects = effects;
this._createInternalEffects();

this._internalEffects = effects.slice();
this._internalEffects.push(DEFAULT_MASK_EFFECT);
if (!effects.some(effect => effect instanceof LightingEffect)) {
this._internalEffects.push(DEFAULT_LIGHTING_EFFECT);
}
}

cleanup() {
Expand All @@ -59,11 +66,4 @@ export default class EffectManager {
this.effects.length = 0;
this._internalEffects.length = 0;
}

_createInternalEffects() {
this._internalEffects = this.effects.slice();
if (!this.effects.some(effect => effect instanceof LightingEffect)) {
this._internalEffects.push(DEFAULT_LIGHTING_EFFECT);
}
}
}
2 changes: 2 additions & 0 deletions modules/core/src/lib/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import type Layer from './layer';
export default class Effect {
id: string;
props: any;
useInPicking: boolean;

constructor(props: {id?: string} = {}) {
const {id = 'effect'} = props;
this.id = id;
this.props = {...props};
this.useInPicking = false;
}

preRender() {}
Expand Down
Loading

0 comments on commit cba5ddf

Please sign in to comment.