UNPKG

@deck.gl-community/layers

Version:

Add-on layers for deck.gl

240 lines (206 loc) 6.59 kB
// 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; }