Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MaskExtension #6554

Merged
merged 78 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from 77 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
717c001
Add MaskExtension
felixpalmer Jan 10, 2022
b1bb46a
Lint fixes
felixpalmer Jan 11, 2022
25832ad
Add missing PROJECTION_MODE export
felixpalmer Jan 11, 2022
0e99ccc
Add 'mask' to all state variables to avoid clashes
felixpalmer Jan 13, 2022
5c35bda
Fix MVTLayer masking
felixpalmer Jan 13, 2022
0acdb0e
Change to 'maskId' API
felixpalmer Jan 17, 2022
ad5455a
Add operation: 'mask' to draw mask layers
felixpalmer Jan 17, 2022
ca1c517
Use positions attribute for bounds calculation
felixpalmer Jan 17, 2022
9ed3c13
Assertions for maskId
felixpalmer Jan 17, 2022
77a0bb7
Extract to getMaskLayer function
felixpalmer Jan 17, 2022
2dbfc46
Checkbox to show/hide mask
felixpalmer Jan 18, 2022
ca63531
Track mask framebuffer using ResourceManager
felixpalmer Jan 18, 2022
6e0c2b6
Move mask test app to test/apps
felixpalmer Jan 18, 2022
8ad051e
Scaffolding for MaskEffect
felixpalmer Jan 18, 2022
8f91c7a
First attempt at MaskEffect.preRender
felixpalmer Jan 18, 2022
aea3afb
Prep for adding mask shader module
felixpalmer Jan 19, 2022
f5827a4
Render into MaskPass
felixpalmer Jan 19, 2022
b1c749f
Add mask shader module
felixpalmer Jan 19, 2022
e1622ac
Pass correct matrices for mask transform
felixpalmer Jan 19, 2022
f49681b
Basic masking working
felixpalmer Jan 19, 2022
32a5028
[Debug] show mask on screen
felixpalmer Jan 19, 2022
9332733
Hovering with mask half-working
felixpalmer Jan 19, 2022
26c73e9
Disable mask when picking
felixpalmer Jan 19, 2022
76147e9
Pass maskByInstance
felixpalmer Jan 19, 2022
d994531
Disable mask debug view
felixpalmer Jan 19, 2022
d8f98dc
Tidy
felixpalmer Jan 19, 2022
7cee11f
Combine mask shaders into one
felixpalmer Jan 19, 2022
8fbf8ab
Tidy mask shaderlib
felixpalmer Jan 19, 2022
20b8a7c
Conditionally discard when mask_enabled
felixpalmer Jan 19, 2022
650655f
maskEnabled prop toggling
felixpalmer Jan 19, 2022
bb843d2
Move matrix split into shaderlib/mask
felixpalmer Jan 19, 2022
843f22b
Remove unused files
felixpalmer Jan 19, 2022
1d21965
Lint
felixpalmer Jan 19, 2022
0c35df9
Handle case of missing maskEnabled prop
felixpalmer Jan 19, 2022
ba9b829
OPERATION enum
felixpalmer Jan 20, 2022
79d9939
Do not hardcode maskId
felixpalmer Jan 20, 2022
981977b
Do not mask without maskProjectionMatrix
felixpalmer Jan 20, 2022
6928e2b
Stopgap measure to prevent picker errors
felixpalmer Jan 20, 2022
0db26ca
Enable mask effect in deck-picker
felixpalmer Jan 20, 2022
dde6220
Simplify mask uniform logic
felixpalmer Jan 20, 2022
0a1af4d
Only pass MaskEffect to deck picker
felixpalmer Jan 20, 2022
88e9537
MaskEffect basic test
felixpalmer Jan 20, 2022
c7892e0
Tests for maskIds
felixpalmer Jan 20, 2022
6dafd41
MaskEffect cleanup
felixpalmer Jan 20, 2022
b0abaca
Mask module installation test
felixpalmer Jan 20, 2022
a80b4cf
Mask effect render test
felixpalmer Jan 21, 2022
c27d696
Merge branch 'master' into felix/mask-extension-wip
felixpalmer Jan 21, 2022
5da57f9
Check that mask is SolidPolygonLayer
felixpalmer Jan 21, 2022
442e11e
Do not directly access attributeManager
felixpalmer Jan 21, 2022
5447342
Migrate MaskPass to TypeScript
felixpalmer Jan 21, 2022
5b3dab8
Don't needlessly create arrays in getBounds
felixpalmer Jan 21, 2022
fc041b2
Disable depthTest and blend for MaskPass
felixpalmer Jan 21, 2022
9748334
Cache mask viewport between frames
felixpalmer Jan 21, 2022
8e06643
Only re-render mask FBO when needed
felixpalmer Jan 21, 2022
b75973b
Check we have attributeManager
felixpalmer Jan 21, 2022
4594cd9
Fix mask render on Google Maps
felixpalmer Jan 21, 2022
c639bab
Pass mask props onto sublayers
felixpalmer Jan 21, 2022
4b8080a
Don't add maskId as default prop to layer
felixpalmer Jan 21, 2022
eac5ce6
Add getBounds() to core Layer class
felixpalmer Jan 25, 2022
b6e8d8c
Move getBounds calculation to Attribute
felixpalmer Jan 25, 2022
815ca37
Enable GeoJsonLayer for masking
felixpalmer Jan 25, 2022
9485966
Calculate composite bounds for CompositeLayer
felixpalmer Jan 25, 2022
5cd920b
Correctly detect when mask data has changed
felixpalmer Jan 25, 2022
8daf4a1
Prevent mask bleeding
felixpalmer Jan 25, 2022
0e09c6a
Lint fix
felixpalmer Jan 25, 2022
4836f64
Do not hardcode WebMercatorViewport for mask
felixpalmer Jan 25, 2022
ef4b1e4
Add orthographic mask example
felixpalmer Jan 25, 2022
1a29eaa
Add getBounds method to Attribute
Pessimistress Jan 25, 2022
bb04560
Remove old Attribute.getBounds()
felixpalmer Jan 25, 2022
603ffa0
Merge branch 'master' into felix/mask-extension-wip
Pessimistress Jan 25, 2022
bc31e2c
Simplify projection calculation
Pessimistress Jan 26, 2022
ed49011
generic implementation of layer.getBounds()
Pessimistress Jan 26, 2022
6596aab
Optimize bounds calculation for mask effect
felixpalmer Jan 26, 2022
0f11863
Prevent error when picking performed without maskBounds
felixpalmer Jan 26, 2022
e175243
Test for MaskPass
felixpalmer Jan 26, 2022
3f7aaaa
Test for maskByInstance
felixpalmer Jan 26, 2022
a3315a4
Lint
felixpalmer Jan 26, 2022
97be55a
Tidy up
felixpalmer Jan 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions modules/core/src/effects/mask/mask-effect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
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});

if (!equals(bounds, this.lastBounds)) {
felixpalmer marked this conversation as resolved.
Show resolved Hide resolved
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);
felixpalmer marked this conversation as resolved.
Show resolved Hide resolved

// 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