Skip to content

Commit

Permalink
Simplify projection calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress committed Jan 26, 2022
1 parent 603ffa0 commit dbee569
Show file tree
Hide file tree
Showing 20 changed files with 303 additions and 446 deletions.
128 changes: 49 additions & 79 deletions modules/core/src/effects/mask/mask-effect.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,75 @@
import {
log,
Texture2D,
ProgramManager
// readPixelsToArray
Texture2D
// , readPixelsToArray
} from '@luma.gl/core';
import MaskPass from '../../passes/mask-pass';
import Effect from '../../lib/effect';
import {default as mask} from '../../shaderlib/mask/mask';
import {getDataViewport, getMaskProjectionMatrix, getMaskViewport} from './utils';
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.programManager = null;
this.dummyMaskMap = null;
this.mask = false;
this.enableForPicking = true;
this.empty = true;
this.useInPicking = true;
}

preRender(gl, {layers, layerFilter, viewports, onViewportActive, views}) {
const maskIds = new Set(
layers.map(({props}) => props.maskId).filter(maskId => maskId !== undefined)
);
if (maskIds.size === 0) {
return;
}
log.assert(maskIds.size === 1, 'Only one mask layer supported, but multiple maskIds specified');

this.mask = true;
const maskId = [...maskIds][0];

if (!this.maskPass) {
this.maskPass = new MaskPass(gl);
this.maskMap = this.maskPass.maskMap;
}
if (!this.programManager) {
this.programManager = ProgramManager.getDefaultProgramManager(gl);
this.programManager.addDefaultModule(mask);
}

if (!this.dummyMaskMap) {
this.dummyMaskMap = new Texture2D(gl, {
width: 1,
height: 1
});
}

// When the mask layer is changed the LayerManager will create a new instance
const maskLayer = this.getMaskLayer(maskId, layers);
const maskChanged = this.maskLayer !== maskLayer;
this.maskLayer = maskLayer;

const {dummyMaskMap, maskPass, maskMap} = this;
const layerViewport = viewports[0];
const maskLayers = layers.filter(l => l.props.operation === OPERATION.MASK);
if (maskLayers.length === 0) {
this.empty = true;
return;
}
this.empty = false;

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

const maskViewport = getMaskViewport(this.dataViewport, layerViewport, maskMap);
if (!maskViewport.equals(this.maskViewport) || maskChanged) {
this.maskViewport = maskViewport;
this.maskProjectionMatrix = getMaskProjectionMatrix(this.maskViewport);
// 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});

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

maskPass.render({
layers,
layerFilter,
viewports: [this.maskViewport],
viewports: [maskViewport],
onViewportActive,
views,
moduleParameters: {
dummyMaskMap,
devicePixelRatio: 1
}
});
Expand Down Expand Up @@ -104,29 +102,11 @@ export default class MaskEffect extends Effect {
}
}

getModuleParameters(layer) {
if (!this.mask) {
return {};
}

const {props} = layer;
const parameters = {
dummyMaskMap: this.dummyMaskMap
getModuleParameters() {
return {
maskMap: this.empty ? this.dummyMaskMap : this.maskMap,
maskBounds: this.maskBounds
};
if (props.maskId) {
// Infer by geometry if 'maskByInstance' prop isn't explictly set
const attributeManager = layer.getAttributeManager();
if (attributeManager && !('maskByInstance' in props)) {
parameters.maskByInstance = 'instancePositions' in attributeManager.attributes;
}
if (!('maskEnabled' in props)) {
parameters.maskEnabled = true;
}

parameters.maskMap = this.maskMap;
parameters.maskProjectionMatrix = this.maskProjectionMatrix;
}
return parameters;
}

cleanup() {
Expand All @@ -140,19 +120,9 @@ export default class MaskEffect extends Effect {
this.maskPass = null;
this.maskMap = null;
}
if (this.mask && this.programManager) {
this.programManager.removeDefaultModule(mask);
this.programManager = null;
}

this.dataViewport = null;
this.maskViewport = null;
this.maskProjectionMatrix = null;
}

getMaskLayer(maskId, layers) {
const maskLayer = layers.find(layer => layer.id === maskId);
log.assert(maskLayer, `{maskId: '${maskId}'} must match the id of another Layer`);
return maskLayer;
this.lastViewport = null;
this.maskBounds = null;
this.empty = true;
}
}
101 changes: 58 additions & 43 deletions modules/core/src/effects/mask/utils.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,72 @@
import {Matrix4} from '@math.gl/core';
import {pixelsToWorld} from '@math.gl/web-mercator';

import OrthographicView from '../../views/orthographic-view';
import WebMercatorViewport from '../../viewports/web-mercator-viewport';
import {fitBounds} from '@math.gl/web-mercator';
/*
* Compute orthographic projection that converts shader common space coordinates into mask clipspace
* 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 getMaskProjectionMatrix({width, height, pixelUnprojectionMatrix}) {
// Unproject corners of viewport into common space (these correspond to the edges
// of the framebuffer we are rendering into)
const [[left, top], [right, bottom]] = [
[0, 0],
[width, height]
].map(pixel => pixelsToWorld(pixel, pixelUnprojectionMatrix));

// Construct orthographic projection that will map common space positions into clipspace
const near = 0.1;
const far = 1000;
return new Matrix4().ortho({left, top, right, bottom, near, far});
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;
}
// Intersect the two
bounds[0] = Math.max(bounds[0], viewportBounds[0]);
bounds[1] = Math.max(bounds[1], viewportBounds[1]);
bounds[2] = Math.min(bounds[2], viewportBounds[2]);
bounds[3] = Math.min(bounds[3], viewportBounds[3]);
return bounds;
}

/*
* Compute viewport to render the mask into, such that it covers an
* area currently visible (extended by a buffer) or the area of the masking
* data, whichever is smaller
*/
export function getMaskViewport(dataViewport, layerViewport, {width, height}) {
const Viewport = layerViewport.constructor;
if (!layerViewport.fitBounds) {
return new Viewport({...layerViewport, x: 0, y: 0, width, height});
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 bounds = layerViewport.getBounds();
let viewport = new Viewport({width, height}).fitBounds([
[bounds[0], bounds[1]],
[bounds[2], bounds[3]]
]);
const {longitude, latitude, zoom} = viewport;
viewport = new Viewport({width, height, longitude, latitude, zoom: zoom - 1});

return dataViewport?.zoom > viewport.zoom ? dataViewport : viewport;
}

export function getDataViewport(layer, {width, height}) {
if (!layer.context.viewport.fitBounds) {
return null;
}

const dataBounds = layer.getBounds();
if (!dataBounds?.flat().every(n => isFinite(n))) {
return null;
}
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]));

const Viewport = layer.context.viewport.constructor;
return new Viewport({width, height}).fitBounds(dataBounds, {
padding: 2
return new OrthographicView().makeViewport({
width,
height,
viewState: {
target: center,
zoom: Math.log2(scale)
}
});
}
1 change: 0 additions & 1 deletion modules/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export {DirectionalLight} from './effects/lighting/directional-light';
export {PointLight} from './effects/lighting/point-light';
export {default as _CameraLight} from './effects/lighting/camera-light';
export {default as _SunLight} from './effects/lighting/sun-light';
export {default as MaskEffect} from './effects/mask/mask-effect';
export {default as PostProcessEffect} from './effects/post-process-effect';

// Passes
Expand Down
29 changes: 1 addition & 28 deletions modules/core/src/lib/composite-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,6 @@ export default class CompositeLayer extends Layer {
extensions,
fetch,
operation,
maskId,
maskByInstance,
maskEnabled,
_subLayerProps: overridingProps
} = this.props;
const newProps = {
Expand All @@ -174,13 +171,9 @@ export default class CompositeLayer extends Layer {
modelMatrix,
extensions,
fetch,
operation,
maskEnabled
operation
};

if (maskId !== undefined) newProps.maskId = maskId;
if (maskByInstance !== undefined) newProps.maskByInstance = maskByInstance;

const overridingSublayerProps = overridingProps && overridingProps[sublayerProps.id];
const overridingSublayerTriggers =
overridingSublayerProps && overridingSublayerProps.updateTriggers;
Expand Down Expand Up @@ -224,26 +217,6 @@ export default class CompositeLayer extends Layer {
return newProps;
}

// Calcaulates the combined bounds of all sublayer (ignoring null bounds)
getBounds() {
const {subLayers} = this.internalState;
let bounds = null;
for (const layer of subLayers) {
const subLayerBounds = layer.getBounds();
if (subLayerBounds) {
if (!bounds) {
bounds = subLayerBounds;
} else {
bounds[0][0] = Math.min(bounds[0][0], subLayerBounds[0][0]);
bounds[0][1] = Math.min(bounds[0][1], subLayerBounds[0][1]);
bounds[1][0] = Math.max(bounds[1][0], subLayerBounds[1][0]);
bounds[1][1] = Math.max(bounds[1][1], subLayerBounds[1][1]);
}
}
}
return bounds;
}

_updateAutoHighlight(info) {
for (const layer of this.getSubLayers()) {
layer.updateAutoHighlight(info);
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/lib/deck.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ export default class Deck {
views: this.viewManager.getViews(),
viewports: this.getViewports(opts),
onViewportActive: this.layerManager.activateViewport,
effects: this.effectManager.getEffects().filter(e => e.enableForPicking),
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);
}
}
}
Loading

0 comments on commit dbee569

Please sign in to comment.