@deck.gl-community/layers
Version:
Add-on layers for deck.gl
240 lines (206 loc) • 6.59 kB
text/typescript
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import type {PathLayerProps} from '@deck.gl/layers';
import {PathLayer} from '@deck.gl/layers';
import type {DefaultProps, LayerContext} from '@deck.gl/core';
import {Framebuffer} from '@luma.gl/core';
import type {Parameters, RenderPipelineParameters, Texture} from '@luma.gl/core';
import {outline} from './outline';
/**
* Unit literal to shader unit number conversion.
*/
export const UNIT = {
common: 0,
meters: 1,
pixels: 2
};
// TODO - this should be built into assembleShaders
function injectShaderCode({source, code = ''}) {
const INJECT_CODE = /}[^{}]*$/;
return source.replace(INJECT_CODE, code.concat('\n}\n'));
}
const VS_CODE = `\
outline_setUV(gl_Position);
outline_setZLevel(instanceZLevel);
`;
const FS_CODE = `\
fragColor = outline_filterColor(fragColor);
`;
const OUTLINE_SHADOWMAP_PARAMETERS: RenderPipelineParameters = {
blend: true,
blendColorSrcFactor: 'one',
blendColorDstFactor: 'one',
blendColorOperation: 'max',
blendAlphaSrcFactor: 'one',
blendAlphaDstFactor: 'one',
blendAlphaOperation: 'max',
depthWriteEnabled: false,
depthCompare: 'always'
};
const OUTLINE_RENDER_PARAMETERS: RenderPipelineParameters = {
blend: false,
depthWriteEnabled: false,
depthCompare: 'always'
};
export type PathOutlineLayerProps<DataT> = PathLayerProps<DataT> & {
dashJustified?: boolean;
getDashArray?: [number, number] | ((d: DataT) => [number, number] | null);
getZLevel?: (d: DataT, index: number) => number;
};
const defaultProps: DefaultProps<PathOutlineLayerProps<any>> = {
getZLevel: () => 0
};
export class PathOutlineLayer<DataT = any, ExtraPropsT = Record<string, unknown>> extends PathLayer<
DataT,
ExtraPropsT & Required<PathOutlineLayerProps<DataT>>
> {
static layerName = 'PathOutlineLayer';
static defaultProps = defaultProps;
state: {
model?: any;
pathTesselator: any;
outlineFramebuffer: Framebuffer;
outlineEmptyTexture: Texture;
} = undefined!;
// Override getShaders to inject the outline module
getShaders() {
const shaders = super.getShaders();
return Object.assign({}, shaders, {
modules: shaders.modules.concat([outline]),
vs: injectShaderCode({source: shaders.vs, code: VS_CODE}),
fs: injectShaderCode({source: shaders.fs, code: FS_CODE})
});
}
// @ts-expect-error PathLayer is missing LayerContext arg
initializeState(context: LayerContext) {
super.initializeState();
const attributeManager = this.getAttributeManager();
if (!attributeManager) {
throw new Error('PathOutlineLayer requires an attribute manager during initialization.');
}
// Create an outline "shadow" map
// TODO - we should create a single outlineMap for all layers
const outlineFramebuffer = context.device.createFramebuffer({
colorAttachments: [
context.device.createTexture({
format: 'rgba8unorm',
width: 1,
height: 1,
mipLevels: 1
})
]
});
const outlineEmptyTexture = context.device.createTexture({
format: 'rgba8unorm',
width: 1,
height: 1,
mipLevels: 1
});
attributeManager.addInstanced({
instanceZLevel: {
size: 1,
type: 'uint8',
accessor: 'getZLevel'
}
});
this.setState({
outlineFramebuffer,
outlineEmptyTexture,
model: this._getModel()
});
}
finalizeState(context: LayerContext) {
this.state.outlineFramebuffer?.destroy();
this.state.outlineEmptyTexture?.destroy();
super.finalizeState(context);
}
// Override draw to add render module
draw({parameters = {} as Parameters}: {parameters?: Parameters; uniforms?: unknown}) {
const model = this.state.model;
const outlineFramebuffer = this.state.outlineFramebuffer;
const outlineEmptyTexture = this.state.outlineEmptyTexture;
if (!model || !outlineFramebuffer || !outlineEmptyTexture) {
return;
}
const viewport = this.context.viewport;
const viewportWidth = Math.max(1, Math.ceil(viewport.width));
const viewportHeight = Math.max(1, Math.ceil(viewport.height));
outlineFramebuffer.resize({width: viewportWidth, height: viewportHeight});
const shadowmapTexture = getFramebufferTexture(outlineFramebuffer);
if (!shadowmapTexture) {
return;
}
const {
jointRounded,
capRounded,
billboard,
miterLimit,
widthUnits,
widthScale,
widthMinPixels,
widthMaxPixels
} = this.props;
const basePathProps = {
jointType: Number(jointRounded),
capType: Number(capRounded),
billboard,
widthUnits: UNIT[widthUnits],
widthScale,
miterLimit,
widthMinPixels,
widthMaxPixels
};
// Render the outline shadowmap (based on segment z orders)
this.setShaderModuleProps({
outline: {
outlineEnabled: true,
outlineRenderShadowmap: true,
outlineShadowmap: outlineEmptyTexture
}
});
model.shaderInputs.setProps({
path: {
...basePathProps,
jointType: 0,
widthScale: widthScale * 1.3
}
});
model.setParameters({...parameters, ...OUTLINE_SHADOWMAP_PARAMETERS});
const shadowRenderPass = this.context.device.beginRenderPass({
id: `${this.props.id}-outline-shadowmap`,
framebuffer: outlineFramebuffer,
parameters: {viewport: [0, 0, viewportWidth, viewportHeight]},
clearColor: [0, 0, 0, 0],
clearDepth: 1,
clearStencil: 0
});
model.draw(shadowRenderPass);
shadowRenderPass.end();
// Now use the outline shadowmap to render the lines (with outlines)
this.setShaderModuleProps({
outline: {
outlineEnabled: true,
outlineRenderShadowmap: false,
outlineShadowmap: shadowmapTexture
}
});
model.shaderInputs.setProps({
path: basePathProps
});
model.setParameters(
isPickingPass(parameters) ? parameters : {...parameters, ...OUTLINE_RENDER_PARAMETERS}
);
model.draw(this.context.renderPass);
}
}
function isPickingPass(parameters: Parameters): boolean {
return parameters.blend === true && parameters.blendAlphaSrcFactor === 'constant';
}
function getFramebufferTexture(framebuffer: Framebuffer): Texture | null {
const colorAttachment = framebuffer.colorAttachments[0];
if (!colorAttachment) {
return null;
}
return 'texture' in colorAttachment ? colorAttachment.texture : colorAttachment;
}