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

Update Effect interface #8681

Merged
merged 2 commits into from Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/upgrade-guide.md
Expand Up @@ -105,6 +105,9 @@ loaders.gl dependencies are updated to v4. Although most version differences are
- If the layer prop `dataTransform` is used to pre-process data, the loaded data object might have changed. For example, `CSVLoader` now yields [a new table format](https://loaders.gl/docs/specifications/category-table).
- For a complete list of breaking changes and improvements, see [loaders.gl 4.0 upgrade guide](https://loaders.gl/docs/upgrade-guide#upgrading-to-v40).

### Others

- Custom effects: the `Effect` interface has changed. `preRender` and `postRender` no longer receives device/context as an argument, implement the `setup()` lifecycle method instead.

## Upgrading from deck.gl v8.8 to v8.9

Expand Down
47 changes: 24 additions & 23 deletions modules/core/src/effects/lighting/lighting-effect.ts
Expand Up @@ -9,7 +9,7 @@ import ShadowPass from '../../passes/shadow-pass';
import shadow from '../../shaderlib/shadow/shadow';

import type Layer from '../../lib/layer';
import type {Effect, PreRenderOptions} from '../../lib/effect';
import type {Effect, EffectContext, PreRenderOptions} from '../../lib/effect';

const DEFAULT_AMBIENT_LIGHT_PROPS = {color: [255, 255, 255], intensity: 1.0};
const DEFAULT_DIRECTIONAL_LIGHT_PROPS = [
Expand All @@ -33,6 +33,7 @@ export default class LightingEffect implements Effect {
id = 'lighting-effect';
props!: LightingEffectProps;
shadowColor: number[] = DEFAULT_SHADOW_COLOR;
context?: EffectContext;

private shadow: boolean = false;
private ambientLight?: AmbientLight | null = null;
Expand All @@ -48,6 +49,22 @@ export default class LightingEffect implements Effect {
this.setProps(props);
}

setup(context: EffectContext) {
this.context = context;
const {device} = context;

if (this.shadow && !this.dummyShadowMap) {
this._createShadowPasses(device);
this.shaderAssembler = ShaderAssembler.getDefaultShaderAssembler();
this.shaderAssembler.addDefaultModule(shadow);

this.dummyShadowMap = device.createTexture({
width: 1,
height: 1
});
}
}

setProps(props: LightingEffectProps) {
this.ambientLight = null;
this.directionalLights = [];
Expand All @@ -74,35 +91,19 @@ export default class LightingEffect implements Effect {
this._applyDefaultLights();

this.shadow = this.directionalLights.some(light => light.shadow);
if (this.context) {
// Create resources if necessary
this.setup(this.context);
}
this.props = props;
}

preRender(
device: Device,
{layers, layerFilter, viewports, onViewportActive, views}: PreRenderOptions
) {
preRender({layers, layerFilter, viewports, onViewportActive, views}: PreRenderOptions) {
if (!this.shadow) return;

// create light matrix every frame to make sure always updated from light source
this.shadowMatrices = this._calculateMatrices();

if (this.shadowPasses.length === 0) {
this._createShadowPasses(device);
}
if (!this.shaderAssembler) {
this.shaderAssembler = ShaderAssembler.getDefaultShaderAssembler();
if (shadow) {
this.shaderAssembler.addDefaultModule(shadow);
}
}

if (!this.dummyShadowMap) {
this.dummyShadowMap = device.createTexture({
width: 1,
height: 1
});
}

for (let i = 0; i < this.shadowPasses.length; i++) {
const shadowPass = this.shadowPasses[i];
shadowPass.render({
Expand Down Expand Up @@ -165,7 +166,7 @@ export default class LightingEffect implements Effect {
this.dummyShadowMap = null;
}

if (this.shadow && this.shaderAssembler) {
if (this.shaderAssembler) {
this.shaderAssembler.removeDefaultModule(shadow);
this.shaderAssembler = null!;
}
Expand Down
17 changes: 10 additions & 7 deletions modules/core/src/effects/post-process-effect.ts
Expand Up @@ -3,7 +3,7 @@ import {normalizeShaderModule, ShaderPass} from '@luma.gl/shadertools';

import ScreenPass from '../passes/screen-pass';

import type {Effect, PostRenderOptions} from '../lib/effect';
import type {Effect, EffectContext, PostRenderOptions} from '../lib/effect';

export default class PostProcessEffect<ShaderPassT extends ShaderPass> implements Effect {
id: string;
Expand All @@ -18,29 +18,32 @@ export default class PostProcessEffect<ShaderPassT extends ShaderPass> implement
this.module = module;
}

setup({device}: EffectContext) {
this.passes = createPasses(device, this.module, this.id);
}

setProps(props: ShaderPassT['props']) {
this.props = props;
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
preRender(): void {}

postRender(device: Device, params: PostRenderOptions): Framebuffer {
const passes = this.passes || createPasses(device, this.module, this.id);
this.passes = passes;
postRender(params: PostRenderOptions): Framebuffer {
const passes = this.passes!;

const {target} = params;
let inputBuffer = params.inputBuffer;
let outputBuffer: Framebuffer | null = params.swapBuffer;

for (let index = 0; index < this.passes.length; index++) {
const isLastPass = index === this.passes.length - 1;
for (let index = 0; index < passes.length; index++) {
const isLastPass = index === passes.length - 1;
if (target !== undefined && isLastPass) {
outputBuffer = target;
}
const moduleSettings = {};
moduleSettings[this.module.name] = this.props;
this.passes[index].render({inputBuffer, outputBuffer, moduleSettings});
passes[index].render({inputBuffer, outputBuffer, moduleSettings});

const switchBuffer = outputBuffer as Framebuffer;
outputBuffer = inputBuffer;
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/index.ts
Expand Up @@ -138,7 +138,7 @@ export type {FilterContext} from './passes/layers-pass';
export type {PickingInfo, GetPickingInfoParams} from './lib/picking/pick-info';
export type {ConstructorOf as _ConstructorOf} from './types/types';
export type {BinaryAttribute} from './lib/attribute/attribute';
export type {Effect, PreRenderOptions, PostRenderOptions} from './lib/effect';
export type {Effect, EffectContext, PreRenderOptions, PostRenderOptions} from './lib/effect';
export type {PickingUniforms, ProjectUniforms} from './shaderlib/index';
export type {DefaultProps} from './lifecycle/prop-types';
export type {LayersPassRenderOptions} from './passes/layers-pass';
Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/lib/deck-picker.ts
Expand Up @@ -339,7 +339,7 @@
}

/** Pick all objects within the given bounding box */
_pickVisibleObjects({

Check warning on line 342 in modules/core/src/lib/deck-picker.ts

View workflow job for this annotation

GitHub Actions / test-node

Method '_pickVisibleObjects' has too many statements (32). Maximum allowed is 25
layers,
views,
viewports,
Expand Down Expand Up @@ -517,7 +517,7 @@

for (const effect of effects) {
if (effect.useInPicking) {
opts.preRenderStats[effect.id] = effect.preRender(this.device, opts);
opts.preRenderStats[effect.id] = effect.preRender(opts);
}
}

Expand Down
15 changes: 5 additions & 10 deletions modules/core/src/lib/deck-renderer.ts
Expand Up @@ -117,7 +117,7 @@ export default class DeckRenderer {
opts.preRenderStats = opts.preRenderStats || {};

for (const effect of effects) {
opts.preRenderStats[effect.id] = effect.preRender(this.device, opts);
Pessimistress marked this conversation as resolved.
Show resolved Hide resolved
opts.preRenderStats[effect.id] = effect.preRender(opts);
if (effect.postRender) {
this.lastPostProcessEffect = effect.id;
}
Expand Down Expand Up @@ -158,17 +158,12 @@ export default class DeckRenderer {
};
for (const effect of effects) {
if (effect.postRender) {
if (effect.id === this.lastPostProcessEffect) {
// Ready to render to final target
params.target = opts.target;
effect.postRender(this.device, params);
break;
}
// If not the last post processing effect, unset the target so that
// it only renders between the swap buffers
params.target = undefined;
const buffer = effect.postRender(this.device, params);
params.inputBuffer = buffer;
params.target = effect.id === this.lastPostProcessEffect ? opts.target : undefined;
const buffer = effect.postRender(params);
// Buffer cannot be null if target is unset
felixpalmer marked this conversation as resolved.
Show resolved Hide resolved
params.inputBuffer = buffer!;
params.swapBuffer = buffer === renderBuffers[0] ? renderBuffers[1] : renderBuffers[0];
}
}
Expand Down
5 changes: 4 additions & 1 deletion modules/core/src/lib/deck.ts
Expand Up @@ -390,7 +390,7 @@
...props.deviceProps,
canvas: this._createCanvas(props)
});
deviceOrPromise.then(device => {

Check warning on line 393 in modules/core/src/lib/deck.ts

View workflow job for this annotation

GitHub Actions / test-node

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
this.device = device;
});
}
Expand All @@ -404,7 +404,7 @@
typedArrayManager.setOptions(props._typedArrayManagerProps);
}

this.animationLoop.start();

Check warning on line 407 in modules/core/src/lib/deck.ts

View workflow job for this annotation

GitHub Actions / test-node

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}

/** Stop rendering and dispose all resources */
Expand Down Expand Up @@ -992,7 +992,10 @@
timeline
});

this.effectManager = new EffectManager();
this.effectManager = new EffectManager({
deck: this,
device: this.device
});

this.deckRenderer = new DeckRenderer(this.device);

Expand Down
22 changes: 13 additions & 9 deletions modules/core/src/lib/effect-manager.ts
@@ -1,6 +1,6 @@
import {deepEqual} from '../utils/deep-equal';
import LightingEffect from '../effects/lighting/lighting-effect';
import type {Effect} from './effect';
import type {Effect, EffectContext} from './effect';

const DEFAULT_LIGHTING_EFFECT = new LightingEffect();

Expand All @@ -17,9 +17,11 @@ export default class EffectManager {
/** Effect instances and order preference pairs, sorted by order */
private _defaultEffects: Effect[] = [];
private _needsRedraw: false | string;
private _context: EffectContext;

constructor() {
constructor(context: EffectContext) {
this.effects = [];
this._context = context;
this._needsRedraw = 'Initial render';
this._setEffects([]);
}
Expand All @@ -36,6 +38,7 @@ export default class EffectManager {
} else {
defaultEffects.splice(index, 0, effect);
}
effect.setup(this._context);
this._setEffects(this.effects);
}
}
Expand Down Expand Up @@ -70,21 +73,22 @@ export default class EffectManager {
const nextEffects: Effect[] = [];
for (const effect of effects) {
const oldEffect = oldEffectsMap[effect.id];
let effectToAdd = effect;
if (oldEffect && oldEffect !== effect) {
if (oldEffect.setProps) {
oldEffect.setProps(effect.props);
nextEffects.push(oldEffect);
effectToAdd = oldEffect;
} else {
oldEffect.cleanup();
nextEffects.push(effect);
oldEffect.cleanup(this._context);
}
} else {
nextEffects.push(effect);
} else if (!oldEffect) {
effect.setup(this._context);
}
nextEffects.push(effectToAdd);
delete oldEffectsMap[effect.id];
}
for (const removedEffectId in oldEffectsMap) {
oldEffectsMap[removedEffectId].cleanup();
oldEffectsMap[removedEffectId].cleanup(this._context);
}
this.effects = nextEffects;

Expand All @@ -98,7 +102,7 @@ export default class EffectManager {

finalize() {
for (const effect of this._resolvedEffects) {
effect.cleanup();
effect.cleanup(this._context);
}

this.effects.length = 0;
Expand Down
26 changes: 21 additions & 5 deletions modules/core/src/lib/effect.ts
@@ -1,3 +1,4 @@
import type Deck from './deck';
import type Layer from './layer';
import type {LayersPassRenderOptions} from '../passes/layers-pass';
import type {Device} from '@luma.gl/core';
Expand All @@ -8,17 +9,32 @@ export type PostRenderOptions = LayersPassRenderOptions & {
inputBuffer: Framebuffer;
swapBuffer: Framebuffer;
};
export type EffectContext = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

device can be obtained from deck. What is the thinking to include them separately? Easier testing?

I see in the followup that deck is needed to add/remove shader modules. If this is the sole purpose of including deck, perhaps it would be better to pass layerManager directly:

export type EffectContext = {
  device: Device;
  layerManager: LayerManager;
};

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Deck.device is not public.
  • It's intentional to route everything through deck, so *manager do not interact with each other directly. If we do change the implementation such as who tracks the shader modules, there will be no breaking change to the effects.

deck: Deck;
device: Device;
};

export interface Effect {
id: string;
props: any;
/** If true, this effect will also be used when rendering to the picking buffer */
useInPicking?: boolean;
/** Effects with smaller value gets executed first. If not provided, will get executed in the order added. */
order?: number;

preRender: (device: Device, opts: PreRenderOptions) => void;
postRender?: (device: Device, opts: PostRenderOptions) => Framebuffer;
getModuleParameters?: (layer: Layer) => any;
// / Render methods
/** Called before layers are rendered to screen */
preRender(opts: PreRenderOptions): void;
/** Called after layers are rendered to screen */
postRender?(opts: PostRenderOptions): Framebuffer | null;
/** Module settings passed to models */
getModuleParameters?(layer: Layer): any;

setProps?: (props: any) => void;
cleanup(): void;
// / Lifecycle methods
/** Called when this effect is added */
setup(context: EffectContext): void;
/** Called when the effect's props are updated. */
setProps?(props: any): void;
/** Called when this effect is removed */
cleanup(context: EffectContext): void;
}
42 changes: 21 additions & 21 deletions modules/extensions/src/collision-filter/collision-filter-effect.ts
@@ -1,7 +1,7 @@
import {Device, Framebuffer, Texture} from '@luma.gl/core';
import {equals} from '@math.gl/core';
import {_deepEqual as deepEqual} from '@deck.gl/core';
import type {Effect, Layer, PreRenderOptions, Viewport} from '@deck.gl/core';
import type {Effect, EffectContext, Layer, PreRenderOptions, Viewport} from '@deck.gl/core';
import CollisionFilterPass from './collision-filter-pass';
import MaskEffect, {MaskPreRenderStats} from '../mask/mask-effect';
// import {debugFBO} from '../utils/debug';
Expand All @@ -27,28 +27,32 @@ export default class CollisionFilterEffect implements Effect {
useInPicking = true;
order = 1;

private context?: EffectContext;
private channels: Record<string, RenderInfo> = {};
private collisionFilterPass?: CollisionFilterPass;
private collisionFBOs: Record<string, Framebuffer> = {};
private dummyCollisionMap?: Texture;
private lastViewport?: Viewport;

preRender(
device: Device,
{
effects: allEffects,
layers,
layerFilter,
viewports,
onViewportActive,
views,
isPicking,
preRenderStats = {}
}: PreRenderOptions
): void {
if (!this.dummyCollisionMap) {
this.dummyCollisionMap = device.createTexture({width: 1, height: 1});
}
setup(context: EffectContext) {
this.context = context;
const {device} = context;
this.dummyCollisionMap = device.createTexture({width: 1, height: 1});
this.collisionFilterPass = new CollisionFilterPass(device, {id: 'default-collision-filter'});
}

preRender({
effects: allEffects,
layers,
layerFilter,
viewports,
onViewportActive,
views,
isPicking,
preRenderStats = {}
}: PreRenderOptions): void {
// This can only be called in preRender() after setup() where context is populated
const {device} = this.context!;

if (isPicking) {
// Do not update on picking pass
Expand All @@ -64,10 +68,6 @@ export default class CollisionFilterEffect implements Effect {
return;
}

if (!this.collisionFilterPass) {
this.collisionFilterPass = new CollisionFilterPass(device, {id: 'default-collision-filter'});
}

// Detect if mask has rendered. TODO: better dependency system for Effects
const effects = allEffects?.filter(e => e.constructor === MaskEffect);
const maskEffectRendered = (preRenderStats['mask-effect'] as MaskPreRenderStats)?.didRender;
Expand Down