Kaynağa Gözat

add optional marking pass

- outlines visible and hidden parts of highlighted/selected groups
- add highlightStrength/selectStrength renderer params
Alexander Rose 3 yıl önce
ebeveyn
işleme
c62f19623c

+ 3 - 0
CHANGELOG.md

@@ -13,6 +13,9 @@ Note that since we don't clearly distinguish between a public and private interf
 - Improved ``StructureElement.Loci.size`` performance (for marking large cellpack models)
 - Fix new ``TransformData`` issues (camera/bounding helper not showing up)
 - Improve marking performance (avoid superfluous calls to ``StructureElement.Loci.isWholeStructure``)
+- Add optional marking pass
+    - Outlines visible and hidden parts of highlighted/selected groups
+    - Add highlightStrength/selectStrength renderer params
 
 ## [v2.2.2] - 2021-08-11
 

+ 5 - 4
src/mol-canvas3d/canvas3d.ts

@@ -38,6 +38,7 @@ import { StereoCamera, StereoCameraParams } from './camera/stereo';
 import { Helper } from './helper/helper';
 import { Passes } from './passes/passes';
 import { shallowEqual } from '../mol-util';
+import { MarkingParams } from './passes/marking';
 
 export const Canvas3DParams = {
     camera: PD.Group({
@@ -80,6 +81,7 @@ export const Canvas3DParams = {
 
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
+    marking: PD.Group(MarkingParams),
     renderer: PD.Group(RendererParams),
     trackball: PD.Group(TrackballControlsParams),
     debug: PD.Group(DebugHelperParams),
@@ -390,7 +392,7 @@ namespace Canvas3D {
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
                     multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
                 } else {
-                    passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing);
+                    passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing, p.marking);
                 }
                 pickHelper.dirty = true;
                 didRender = true;
@@ -636,6 +638,7 @@ namespace Canvas3D {
                 viewport: p.viewport,
 
                 postprocessing: { ...p.postprocessing },
+                marking: { ...p.marking },
                 multiSample: { ...p.multiSample },
                 renderer: { ...renderer.props },
                 trackball: { ...controls.props },
@@ -771,6 +774,7 @@ namespace Canvas3D {
                 }
 
                 if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
+                if (props.marking) Object.assign(p.marking, props.marking);
                 if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
                 if (props.renderer) renderer.setProps(props.renderer);
                 if (props.trackball) controls.setProps(props.trackball);
@@ -835,9 +839,6 @@ namespace Canvas3D {
                 height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
                 y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
                 width = Math.round(p.viewport.params.width * gl.drawingBufferWidth);
-                // if (x + width >= gl.drawingBufferWidth) width = gl.drawingBufferWidth - x;
-                // if (y + height >= gl.drawingBufferHeight) height = gl.drawingBufferHeight - y - 1;
-                // console.log({ x, y, width, height });
             }
 
             if (oldX !== x || oldY !== y || oldWidth !== width || oldHeight !== height) {

+ 26 - 5
src/mol-canvas3d/passes/draw.ts

@@ -26,6 +26,7 @@ import { copy_frag } from '../../mol-gl/shader/copy.frag';
 import { StereoCamera } from '../camera/stereo';
 import { WboitPass } from './wboit';
 import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
+import { MarkingPass, MarkingProps } from './marking';
 
 const DepthMergeSchema = {
     ...QuadSchema,
@@ -92,6 +93,7 @@ export class DrawPass {
     private copyFboPostprocessing: CopyRenderable
 
     private wboit: WboitPass | undefined
+    private readonly marking: MarkingPass
     readonly postprocessing: PostprocessingPass
     private readonly antialiasing: AntialiasingPass
 
@@ -122,6 +124,7 @@ export class DrawPass {
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
 
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
+        this.marking = new MarkingPass(webgl, width, height);
         this.postprocessing = new PostprocessingPass(webgl, this);
         this.antialiasing = new AntialiasingPass(webgl, this);
 
@@ -162,6 +165,7 @@ export class DrawPass {
                 this.wboit.setSize(width, height);
             }
 
+            this.marking.setSize(width, height);
             this.postprocessing.setSize(width, height);
             this.antialiasing.setSize(width, height);
         }
@@ -281,10 +285,11 @@ export class DrawPass {
         renderer.renderBlendedTransparent(scene.primitives, camera, null);
     }
 
-    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
+    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps, markingProps: MarkingProps) {
         const volumeRendering = scene.volumes.renderables.length > 0;
         const postprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
         const antialiasingEnabled = AntialiasingPass.isEnabled(postprocessingProps);
+        const markingEnabled = MarkingPass.isEnabled(markingProps);
 
         const { x, y, width, height } = camera.viewport;
         renderer.setViewport(x, y, width, height);
@@ -309,6 +314,22 @@ export class DrawPass {
             this.drawTarget.bind();
         }
 
+        if (markingEnabled) {
+            const markingDepthTest = markingProps.ghostEdgeStrength < 1;
+            if (markingDepthTest) {
+                this.marking.depthTarget.bind();
+                renderer.clear(false);
+                renderer.renderMarkingDepth(scene.primitives, camera, null);
+            }
+
+            this.marking.maskTarget.bind();
+            renderer.clear(false);
+            renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
+
+            this.marking.update(markingProps);
+            this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget);
+        }
+
         if (helper.debug.isEnabled) {
             helper.debug.syncVisibility();
             renderer.renderBlended(helper.debug.scene, camera, null);
@@ -338,15 +359,15 @@ export class DrawPass {
         this.webgl.gl.flush();
     }
 
-    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
+    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps, markingProps: MarkingProps) {
         renderer.setTransparentBackground(transparentBackground);
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
 
         if (StereoCamera.is(camera)) {
-            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
-            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
+            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);
+            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);
         } else {
-            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
+            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps, markingProps);
         }
     }
 

+ 3 - 1
src/mol-canvas3d/passes/image.ts

@@ -17,11 +17,13 @@ import { Viewport } from '../camera/util';
 import { PixelData } from '../../mol-util/image';
 import { Helper } from '../helper/helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
+import { MarkingParams } from './marking';
 
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
+    marking: PD.Group(MarkingParams),
 
     cameraHelper: PD.Group(CameraHelperParams),
 };
@@ -85,7 +87,7 @@ export class ImagePass {
             this.multiSampleHelper.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props);
             this._colorTarget = this.multiSamplePass.colorTarget;
         } else {
-            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props.postprocessing);
+            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props.postprocessing, this.props.marking);
             this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
         }
     }

+ 194 - 0
src/mol-canvas3d/passes/marking.ts

@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
+import { ShaderCode } from '../../mol-gl/shader-code';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
+import { Texture } from '../../mol-gl/webgl/texture';
+import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { ValueCell } from '../../mol-util';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { quad_vert } from '../../mol-gl/shader/quad.vert';
+import { overlay_frag } from '../../mol-gl/shader/marking/overlay.frag';
+import { Viewport } from '../camera/util';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
+import { Color } from '../../mol-util/color';
+import { edge_frag } from '../../mol-gl/shader/marking/edge.frag';
+
+export const MarkingParams = {
+    enabled: PD.Boolean(false),
+    highlightEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(1.0, 0.4, 0.6), 1.0)),
+    selectEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(0.2, 1.0, 0.1), 1.0)),
+    edgeScale: PD.Numeric(1, { min: 1, max: 3, step: 1 }, { description: 'Thickness of the edge.' }),
+    ghostEdgeStrength: PD.Numeric(0.3, { min: 0, max: 1, step: 0.1 }, { description: 'Opacity of the hidden edges that are covered by other geometry. When set to 1, one less geometry render pass is done.' }),
+    innerEdgeFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Factor to multiply the inner edge color with - for added contrast.' }),
+};
+export type MarkingProps = PD.Values<typeof MarkingParams>
+
+export class MarkingPass {
+    static isEnabled(props: MarkingProps) {
+        return props.enabled;
+    }
+
+    readonly depthTarget: RenderTarget
+    readonly maskTarget: RenderTarget
+    private readonly edgesTarget: RenderTarget
+
+    private readonly edge: EdgeRenderable
+    private readonly overlay: OverlayRenderable
+
+    constructor(private webgl: WebGLContext, width: number, height: number) {
+        this.depthTarget = webgl.createRenderTarget(width, height);
+        this.maskTarget = webgl.createRenderTarget(width, height);
+        this.edgesTarget = webgl.createRenderTarget(width, height);
+
+        this.edge = getEdgeRenderable(webgl, this.maskTarget.texture);
+        this.overlay = getOverlayRenderable(webgl, this.edgesTarget.texture);
+    }
+
+    private setEdgeState(viewport: Viewport) {
+        const { gl, state } = this.webgl;
+
+        state.enable(gl.SCISSOR_TEST);
+        state.enable(gl.BLEND);
+        state.blendFunc(gl.ONE, gl.ONE);
+        state.blendEquation(gl.FUNC_ADD);
+        state.disable(gl.DEPTH_TEST);
+        state.depthMask(false);
+
+        const { x, y, width, height } = viewport;
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+
+        state.clearColor(0, 0, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+    }
+
+    private setOverlayState(viewport: Viewport) {
+        const { gl, state } = this.webgl;
+
+        state.enable(gl.SCISSOR_TEST);
+        state.enable(gl.BLEND);
+        state.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
+        state.blendEquation(gl.FUNC_ADD);
+        state.disable(gl.DEPTH_TEST);
+        state.depthMask(false);
+
+        const { x, y, width, height } = viewport;
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+    }
+
+    setSize(width: number, height: number) {
+        const w = this.depthTarget.getWidth();
+        const h = this.depthTarget.getHeight();
+
+        if (width !== w || height !== h) {
+            this.depthTarget.setSize(width, height);
+            this.maskTarget.setSize(width, height);
+            this.edgesTarget.setSize(width, height);
+
+            ValueCell.update(this.edge.values.uTexSizeInv, Vec2.set(this.edge.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
+            ValueCell.update(this.overlay.values.uTexSizeInv, Vec2.set(this.overlay.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
+        }
+    }
+
+    update(props: MarkingProps) {
+        const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength } = props;
+
+        const { values: edgeValues } = this.edge;
+        const _edgeScale = Math.round(edgeScale * this.webgl.pixelRatio);
+        if (edgeValues.dEdgeScale.ref.value !== _edgeScale) {
+            ValueCell.update(edgeValues.dEdgeScale, _edgeScale);
+            this.edge.update();
+        }
+
+        const { values: overlayValues } = this.overlay;
+        ValueCell.update(overlayValues.uHighlightEdgeColor, Color.toVec3Normalized(overlayValues.uHighlightEdgeColor.ref.value, highlightEdgeColor));
+        ValueCell.update(overlayValues.uSelectEdgeColor, Color.toVec3Normalized(overlayValues.uSelectEdgeColor.ref.value, selectEdgeColor));
+        ValueCell.update(overlayValues.uInnerEdgeFactor, innerEdgeFactor);
+        ValueCell.update(overlayValues.uGhostEdgeStrength, ghostEdgeStrength);
+    }
+
+    render(viewport: Viewport, target: RenderTarget | undefined) {
+        this.edgesTarget.bind();
+        this.setEdgeState(viewport);
+        this.edge.render();
+
+        if (target) {
+            target.bind();
+        } else {
+            this.webgl.unbindFramebuffer();
+        }
+        this.setOverlayState(viewport);
+        this.overlay.render();
+    }
+}
+
+//
+
+const EdgeSchema = {
+    ...QuadSchema,
+    tMaskTexture: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    uTexSizeInv: UniformSpec('v2'),
+    dEdgeScale: DefineSpec('number'),
+};
+const EdgeShaderCode = ShaderCode('edge', quad_vert, edge_frag);
+type EdgeRenderable = ComputeRenderable<Values<typeof EdgeSchema>>
+
+function getEdgeRenderable(ctx: WebGLContext, maskTexture: Texture): EdgeRenderable {
+    const width = maskTexture.getWidth();
+    const height = maskTexture.getHeight();
+
+    const values: Values<typeof EdgeSchema> = {
+        ...QuadValues,
+        tMaskTexture: ValueCell.create(maskTexture),
+        uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
+        dEdgeScale: ValueCell.create(1),
+    };
+
+    const schema = { ...EdgeSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', EdgeShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+//
+
+const OverlaySchema = {
+    ...QuadSchema,
+    tEdgeTexture: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    uTexSizeInv: UniformSpec('v2'),
+    uHighlightEdgeColor: UniformSpec('v3'),
+    uSelectEdgeColor: UniformSpec('v3'),
+    uGhostEdgeStrength: UniformSpec('f'),
+    uInnerEdgeFactor: UniformSpec('f'),
+};
+const OverlayShaderCode = ShaderCode('overlay', quad_vert, overlay_frag);
+type OverlayRenderable = ComputeRenderable<Values<typeof OverlaySchema>>
+
+function getOverlayRenderable(ctx: WebGLContext, edgeTexture: Texture): OverlayRenderable {
+    const width = edgeTexture.getWidth();
+    const height = edgeTexture.getHeight();
+
+    const values: Values<typeof OverlaySchema> = {
+        ...QuadValues,
+        tEdgeTexture: ValueCell.create(edgeTexture),
+        uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
+        uHighlightEdgeColor: ValueCell.create(Vec3()),
+        uSelectEdgeColor: ValueCell.create(Vec3()),
+        uGhostEdgeStrength: ValueCell.create(0),
+        uInnerEdgeFactor: ValueCell.create(0),
+    };
+
+    const schema = { ...OverlaySchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', OverlayShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}

+ 9 - 5
src/mol-canvas3d/passes/multi-sample.ts

@@ -22,9 +22,9 @@ import { Renderer } from '../../mol-gl/renderer';
 import { Scene } from '../../mol-gl/scene';
 import { Helper } from '../helper/helper';
 import { StereoCamera } from '../camera/stereo';
-
 import { quad_vert } from '../../mol-gl/shader/quad.vert';
 import { compose_frag } from '../../mol-gl/shader/compose.frag';
+import { MarkingProps } from './marking';
 
 const ComposeSchema = {
     ...QuadSchema,
@@ -55,7 +55,11 @@ export const MultiSampleParams = {
 };
 export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
 
-type Props = { multiSample: MultiSampleProps, postprocessing: PostprocessingProps }
+type Props = {
+    multiSample: MultiSampleProps
+    postprocessing: PostprocessingProps
+    marking: MarkingProps
+}
 
 export class MultiSamplePass {
     static isEnabled(props: MultiSampleProps) {
@@ -144,7 +148,7 @@ export class MultiSamplePass {
             ValueCell.update(compose.values.uWeight, sampleWeight);
 
             // render scene
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing, props.marking);
 
             // compose rendered scene with compose target
             composeTarget.bind();
@@ -194,7 +198,7 @@ export class MultiSamplePass {
         const sampleWeight = 1.0 / offsetList.length;
 
         if (sampleIndex === -1) {
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing, props.marking);
             ValueCell.update(compose.values.uWeight, 1.0);
             ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             compose.update();
@@ -222,7 +226,7 @@ export class MultiSamplePass {
                 camera.update();
 
                 // render scene
-                drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
+                drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing, props.marking);
 
                 // compose rendered scene with compose target
                 composeTarget.bind();

+ 22 - 1
src/mol-geo/geometry/marker-data.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -11,18 +11,35 @@ import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 export type MarkerData = {
     tMarker: ValueCell<TextureImage<Uint8Array>>
     uMarkerTexDim: ValueCell<Vec2>
+    markerAverage: ValueCell<number>
+    markerStatus: ValueCell<number>
+}
+
+export function getMarkersAverage(array: Uint8Array, count: number): number {
+    if (count === 0) return 0;
+    let sum = 0;
+    for (let i = 0; i < count; ++i) {
+        if (array[i]) sum += 1;
+    }
+    return sum / count;
 }
 
 export function createMarkers(count: number, markerData?: MarkerData): MarkerData {
     const markers = createTextureImage(Math.max(1, count), 1, Uint8Array, markerData && markerData.tMarker.ref.value.array);
+    const average = getMarkersAverage(markers.array, count);
+    const status = average === 0 ? 0 : -1;
     if (markerData) {
         ValueCell.update(markerData.tMarker, markers);
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height));
+        ValueCell.updateIfChanged(markerData.markerAverage, average);
+        ValueCell.updateIfChanged(markerData.markerStatus, status);
         return markerData;
     } else {
         return {
             tMarker: ValueCell.create(markers),
             uMarkerTexDim: ValueCell.create(Vec2.create(markers.width, markers.height)),
+            markerAverage: ValueCell.create(average),
+            markerStatus: ValueCell.create(status),
         };
     }
 }
@@ -32,11 +49,15 @@ export function createEmptyMarkers(markerData?: MarkerData): MarkerData {
     if (markerData) {
         ValueCell.update(markerData.tMarker, emptyMarkerTexture);
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(1, 1));
+        ValueCell.updateIfChanged(markerData.markerAverage, 0);
+        ValueCell.updateIfChanged(markerData.markerStatus, 0);
         return markerData;
     } else {
         return {
             tMarker: ValueCell.create(emptyMarkerTexture),
             uMarkerTexDim: ValueCell.create(Vec2.create(1, 1)),
+            markerAverage: ValueCell.create(0),
+            markerStatus: ValueCell.create(0),
         };
     }
 }

+ 6 - 6
src/mol-gl/_spec/renderer.spec.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -134,17 +134,17 @@ describe('renderer', () => {
         scene.commit();
         expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
         expect(ctx.stats.resourceCounts.texture).toBe(7);
-        expect(ctx.stats.resourceCounts.vertexArray).toBe(6);
-        expect(ctx.stats.resourceCounts.program).toBe(6);
-        expect(ctx.stats.resourceCounts.shader).toBe(12);
+        expect(ctx.stats.resourceCounts.vertexArray).toBe(8);
+        expect(ctx.stats.resourceCounts.program).toBe(8);
+        expect(ctx.stats.resourceCounts.shader).toBe(16);
 
         scene.remove(points);
         scene.commit();
         expect(ctx.stats.resourceCounts.attribute).toBe(0);
         expect(ctx.stats.resourceCounts.texture).toBe(0);
         expect(ctx.stats.resourceCounts.vertexArray).toBe(0);
-        expect(ctx.stats.resourceCounts.program).toBe(6);
-        expect(ctx.stats.resourceCounts.shader).toBe(12);
+        expect(ctx.stats.resourceCounts.program).toBe(8);
+        expect(ctx.stats.resourceCounts.shader).toBe(16);
 
         ctx.resources.destroy();
         expect(ctx.stats.resourceCounts.program).toBe(0);

+ 5 - 1
src/mol-gl/renderable/schema.ts

@@ -106,7 +106,6 @@ export type RenderableSchema = {
 }
 export type RenderableValues = { readonly [k: string]: ValueCell<any> }
 
-
 //
 
 export const GlobalUniformSchema = {
@@ -161,10 +160,13 @@ export const GlobalUniformSchema = {
 
     uHighlightColor: UniformSpec('v3'),
     uSelectColor: UniformSpec('v3'),
+    uHighlightStrength: UniformSpec('f'),
+    uSelectStrength: UniformSpec('f'),
 
     uXrayEdgeFalloff: UniformSpec('f'),
 
     uRenderWboit: UniformSpec('b'),
+    uMarkingDepthTest: UniformSpec('b'),
 } as const;
 export type GlobalUniformSchema = typeof GlobalUniformSchema
 export type GlobalUniformValues = Values<GlobalUniformSchema>
@@ -210,6 +212,8 @@ export type SizeValues = Values<SizeSchema>
 export const MarkerSchema = {
     uMarkerTexDim: UniformSpec('v2'),
     tMarker: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    markerAverage: ValueSpec('number'),
+    markerStatus: ValueSpec('number'),
 } as const;
 export type MarkerSchema = typeof MarkerSchema
 export type MarkerValues = Values<MarkerSchema>

+ 66 - 14
src/mol-gl/renderer.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -48,6 +48,8 @@ interface Renderer {
 
     renderPick: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, depthTexture: Texture | null) => void
     renderDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
@@ -76,6 +78,8 @@ export const RendererParams = {
 
     highlightColor: PD.Color(Color.fromNormalizedRgb(1.0, 0.4, 0.6)),
     selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
+    highlightStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }),
+    selectStrength: PD.Numeric(0.7, { min: 0.0, max: 1.0, step: 0.1 }),
 
     xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
 
@@ -242,6 +246,7 @@ namespace Renderer {
             uFogColor: ValueCell.create(bgColor),
 
             uRenderWboit: ValueCell.create(false),
+            uMarkingDepthTest: ValueCell.create(false),
 
             uTransparentBackground: ValueCell.create(false),
 
@@ -267,6 +272,8 @@ namespace Renderer {
 
             uHighlightColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.highlightColor)),
             uSelectColor: ValueCell.create(Color.toVec3Normalized(Vec3(), p.selectColor)),
+            uHighlightStrength: ValueCell.create(p.highlightStrength),
+            uSelectStrength: ValueCell.create(p.selectStrength),
 
             uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
         };
@@ -375,7 +382,7 @@ namespace Renderer {
             ValueCell.updateIfChanged(globalUniforms.uTransparentBackground, transparentBackground);
         };
 
-        const updateInternal = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, renderWboit: boolean) => {
+        const updateInternal = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, renderWboit: boolean, markingDepthTest: boolean) => {
             arrayMapUpsert(sharedTexturesList, 'tDepth', depthTexture || nullDepthTexture);
 
             ValueCell.update(globalUniforms.uModel, group.view);
@@ -385,6 +392,7 @@ namespace Renderer {
             ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection));
 
             ValueCell.updateIfChanged(globalUniforms.uRenderWboit, renderWboit);
+            ValueCell.updateIfChanged(globalUniforms.uMarkingDepthTest, markingDepthTest);
 
             state.enable(gl.SCISSOR_TEST);
             state.colorMask(true, true, true, true);
@@ -402,7 +410,7 @@ namespace Renderer {
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
 
-            updateInternal(group, camera, depthTexture, false);
+            updateInternal(group, camera, depthTexture, false, false);
 
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -417,7 +425,7 @@ namespace Renderer {
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
 
-            updateInternal(group, camera, depthTexture, false);
+            updateInternal(group, camera, depthTexture, false, false);
 
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -425,6 +433,40 @@ namespace Renderer {
             }
         };
 
+        const renderMarkingDepth = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            state.disable(gl.BLEND);
+            state.enable(gl.DEPTH_TEST);
+            state.depthMask(true);
+
+            updateInternal(group, camera, depthTexture, false, false);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+
+                if (r.values.markerAverage.ref.value !== 1) {
+                    renderObject(renderables[i], 'markingDepth');
+                }
+            }
+        };
+
+        const renderMarkingMask = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            state.disable(gl.BLEND);
+            state.enable(gl.DEPTH_TEST);
+            state.depthMask(true);
+
+            updateInternal(group, camera, depthTexture, false, !!depthTexture);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+
+                if (r.values.markerAverage.ref.value > 0) {
+                    renderObject(renderables[i], 'markingMask');
+                }
+            }
+        };
+
         const renderBlended = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
             renderBlendedOpaque(group, camera, depthTexture);
             renderBlendedTransparent(group, camera, depthTexture);
@@ -435,7 +477,7 @@ namespace Renderer {
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
 
-            updateInternal(group, camera, depthTexture, false);
+            updateInternal(group, camera, depthTexture, false, false);
 
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -449,7 +491,7 @@ namespace Renderer {
         const renderBlendedTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
             state.enable(gl.DEPTH_TEST);
 
-            updateInternal(group, camera, depthTexture, false);
+            updateInternal(group, camera, depthTexture, false, false);
 
             const { renderables } = group;
 
@@ -481,13 +523,13 @@ namespace Renderer {
             state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
             state.enable(gl.BLEND);
 
-            updateInternal(group, camera, depthTexture, false);
+            updateInternal(group, camera, depthTexture, false, false);
 
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
 
-                // TODO: simplify, handle on renderable.state???
+                // TODO: simplify, handle in renderable.state???
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
                 if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && !r.values.dXrayShaded?.ref.value) {
@@ -500,13 +542,13 @@ namespace Renderer {
             state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
             state.enable(gl.BLEND);
 
-            updateInternal(group, camera, depthTexture, false);
+            updateInternal(group, camera, depthTexture, false, false);
 
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
 
-                // TODO: simplify, handle on renderable.state???
+                // TODO: simplify, handle in renderable.state???
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
                 if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dXrayShaded?.ref.value) {
@@ -520,13 +562,13 @@ namespace Renderer {
             state.enable(gl.DEPTH_TEST);
             state.depthMask(true);
 
-            updateInternal(group, camera, depthTexture, false);
+            updateInternal(group, camera, depthTexture, false, false);
 
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
 
-                // TODO: simplify, handle on renderable.state???
+                // TODO: simplify, handle in renderable.state???
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
                 if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dRenderMode?.ref.value !== 'volume' && !r.values.dPointFilledCircle?.ref.value && !r.values.dXrayShaded?.ref.value) {
@@ -536,13 +578,13 @@ namespace Renderer {
         };
 
         const renderWboitTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
-            updateInternal(group, camera, depthTexture, true);
+            updateInternal(group, camera, depthTexture, true, false);
 
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
 
-                // TODO: simplify, handle on renderable.state???
+                // TODO: simplify, handle in renderable.state???
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
                 if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dRenderMode?.ref.value === 'volume' || r.values.dPointFilledCircle?.ref.value || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
@@ -577,6 +619,8 @@ namespace Renderer {
 
             renderPick,
             renderDepth,
+            renderMarkingDepth,
+            renderMarkingMask,
             renderBlended,
             renderBlendedOpaque,
             renderBlendedTransparent,
@@ -618,6 +662,14 @@ namespace Renderer {
                     p.selectColor = props.selectColor;
                     ValueCell.update(globalUniforms.uSelectColor, Color.toVec3Normalized(globalUniforms.uSelectColor.ref.value, p.selectColor));
                 }
+                if (props.highlightStrength !== undefined && props.highlightStrength !== p.highlightStrength) {
+                    p.highlightStrength = props.highlightStrength;
+                    ValueCell.update(globalUniforms.uHighlightStrength, p.highlightStrength);
+                }
+                if (props.selectStrength !== undefined && props.selectStrength !== p.selectStrength) {
+                    p.selectStrength = props.selectStrength;
+                    ValueCell.update(globalUniforms.uSelectStrength, p.selectStrength);
+                }
 
                 if (props.xrayEdgeFalloff !== undefined && props.xrayEdgeFalloff !== p.xrayEdgeFalloff) {
                     p.xrayEdgeFalloff = props.xrayEdgeFalloff;

+ 0 - 2
src/mol-gl/shader-code.ts

@@ -65,7 +65,6 @@ import { size_vert_params } from './shader/chunks/size-vert-params.glsl';
 import { texture3d_from_1d_trilinear } from './shader/chunks/texture3d-from-1d-trilinear.glsl';
 import { texture3d_from_2d_linear } from './shader/chunks/texture3d-from-2d-linear.glsl';
 import { texture3d_from_2d_nearest } from './shader/chunks/texture3d-from-2d-nearest.glsl';
-import { wboit_params } from './shader/chunks/wboit-params.glsl';
 import { wboit_write } from './shader/chunks/wboit-write.glsl';
 
 const ShaderChunks: { [k: string]: string } = {
@@ -99,7 +98,6 @@ const ShaderChunks: { [k: string]: string } = {
     texture3d_from_1d_trilinear,
     texture3d_from_2d_linear,
     texture3d_from_2d_nearest,
-    wboit_params,
     wboit_write
 };
 

+ 4 - 3
src/mol-gl/shader/chunks/apply-marker-color.glsl.ts

@@ -2,10 +2,11 @@ export const apply_marker_color = `
 float marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win
 if (marker > 0.1) {
     if (intMod(marker, 2.0) > 0.1) {
-        gl_FragColor.rgb = mix(uHighlightColor, gl_FragColor.rgb, 0.3);
-        gl_FragColor.a = max(0.02, gl_FragColor.a); // for direct-volume rendering
+        gl_FragColor.rgb = mix(gl_FragColor.rgb, uHighlightColor, uHighlightStrength);
+        gl_FragColor.a = max(gl_FragColor.a, uHighlightStrength * 0.002); // for direct-volume rendering
     } else {
-        gl_FragColor.rgb = mix(uSelectColor, gl_FragColor.rgb, 0.3);
+        gl_FragColor.rgb = mix(gl_FragColor.rgb, uSelectColor, uSelectStrength);
+        gl_FragColor.a = max(gl_FragColor.a, uSelectStrength * 0.002); // for direct-volume rendering
     }
 }
 `;

+ 17 - 0
src/mol-gl/shader/chunks/assign-material-color.glsl.ts

@@ -20,6 +20,23 @@ export const assign_material_color = `
     #else
         vec4 material = packDepthToRGBA(gl_FragCoord.z);
     #endif
+#elif defined(dRenderVariant_markingDepth)
+    if (vMarker > 0.0)
+        discard;
+    #ifdef enabledFragDepth
+        vec4 material = packDepthToRGBA(gl_FragDepthEXT);
+    #else
+        vec4 material = packDepthToRGBA(gl_FragCoord.z);
+    #endif
+#elif defined(dRenderVariant_markingMask)
+    if (vMarker == 0.0)
+        discard;
+    float depthTest = 1.0;
+    if (uMarkingDepthTest) {
+        depthTest = (fragmentDepth >= getDepth(gl_FragCoord.xy / uDrawingBufferSize)) ? 1.0 : 0.0;
+    }
+    bool isHighlight = intMod(floor(vMarker * 255.0 + 0.5), 2.0) > 0.1;
+    vec4 material = vec4(0.0, depthTest, isHighlight ? 1.0 : 0.0, 1.0);
 #endif
 
 // apply screendoor transparency

+ 18 - 0
src/mol-gl/shader/chunks/common-frag-params.glsl.ts

@@ -21,6 +21,8 @@ uniform int uGroupCount;
 
 uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
+uniform float uHighlightStrength;
+uniform float uSelectStrength;
 #if __VERSION__ == 100
     varying float vMarker;
 #else
@@ -52,4 +54,20 @@ bool interior;
 uniform float uXrayEdgeFalloff;
 
 uniform mat4 uProjection;
+
+uniform bool uRenderWboit;
+uniform bool uMarkingDepthTest;
+
+uniform sampler2D tDepth;
+uniform vec2 uDrawingBufferSize;
+
+float getDepth(const in vec2 coords) {
+    // always packed due to merged depth from primitives and volumes
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
+}
+
+float calcDepth(const in vec3 pos) {
+    vec2 clipZW = pos.z * uProjection[2].zw + uProjection[3].zw;
+    return 0.5 + 0.5 * clipZW.x / clipZW.y;
+}
 `;

+ 4 - 0
src/mol-gl/shader/chunks/common.glsl.ts

@@ -9,6 +9,10 @@ export const common = `
     #define dRenderVariant_pick
 #endif
 
+#if defined(dRenderVariant_markingDepth) || defined(dRenderVariant_markingMask)
+    #define dRenderVariant_marking
+#endif
+
 #if defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance) || defined(dColorType_vertex) || defined(dColorType_vertexInstance)
     #define dColorType_texture
 #endif

+ 0 - 20
src/mol-gl/shader/chunks/wboit-params.glsl.ts

@@ -1,20 +0,0 @@
-export const wboit_params = `
-#if defined(dRenderVariant_colorWboit)
-    #if !defined(dRenderMode_volume) && !defined(dRenderMode_isosurface)
-        uniform sampler2D tDepth;
-        uniform vec2 uDrawingBufferSize;
-
-        float getDepth(const in vec2 coords) {
-            // always packed due to merged depth from primitives and volumes
-            return unpackRGBAToDepth(texture2D(tDepth, coords));
-        }
-    #endif
-#endif
-
-uniform bool uRenderWboit;
-
-float calcDepth(const in vec3 pos) {
-    vec2 clipZW = pos.z * uProjection[2].zw + uProjection[3].zw;
-    return 0.5 + 0.5 * clipZW.x / clipZW.y;
-}
-`;

+ 3 - 2
src/mol-gl/shader/cylinders.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -24,7 +24,6 @@ uniform vec3 uCameraPosition;
 #include color_frag_params
 #include light_frag_params
 #include common_clip
-#include wboit_params
 
 // adapted from https://www.shadertoy.com/view/4lcSRn
 // The MIT License, Copyright 2016 Inigo Quilez
@@ -121,6 +120,8 @@ void main() {
         gl_FragColor = material;
     #elif defined(dRenderVariant_depth)
         gl_FragColor = material;
+    #elif defined(dRenderVariant_marking)
+        gl_FragColor = material;
     #elif defined(dRenderVariant_color)
         #ifdef dIgnoreLight
             gl_FragColor = material;

+ 13 - 1
src/mol-gl/shader/direct-volume.frag.ts

@@ -53,6 +53,8 @@ uniform int uGroupCount;
 
 uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
+uniform float uHighlightStrength;
+uniform float uSelectStrength;
 uniform vec2 uMarkerTexDim;
 uniform sampler2D tMarker;
 
@@ -69,6 +71,8 @@ uniform bool uInteriorColorFlag;
 uniform vec3 uInteriorColor;
 bool interior;
 
+uniform bool uRenderWboit;
+
 uniform float uNear;
 uniform float uFar;
 uniform float uIsOrtho;
@@ -122,7 +126,10 @@ uniform mat4 uCartnToUnit;
     }
 #endif
 
-#include wboit_params
+float calcDepth(const in vec3 pos) {
+    vec2 clipZW = pos.z * uProjection[2].zw + uProjection[3].zw;
+    return 0.5 + 0.5 * clipZW.x / clipZW.y;
+}
 
 vec4 transferFunction(float value) {
     return texture2D(tTransferTex, vec2(value, 0.0));
@@ -432,6 +439,11 @@ void main() {
     if (gl_FrontFacing)
         discard;
 
+    #ifdef dRenderVariant_marking
+        // not supported
+        discard;
+    #endif
+
     #if defined(dRenderVariant_pick) || defined(dRenderVariant_depth)
         #if defined(dRenderMode_volume)
             // always ignore pick & depth for volume

+ 18 - 5
src/mol-gl/shader/image.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -11,7 +11,6 @@ precision highp int;
 #include read_from_texture
 #include common_frag_params
 #include common_clip
-#include wboit_params
 
 uniform vec2 uImageTexDim;
 uniform sampler2D tImageTex;
@@ -105,7 +104,6 @@ void main() {
     #if defined(dRenderVariant_pick)
         if (imageData.a < 0.3)
             discard;
-
         #if defined(dRenderVariant_pickObject)
             gl_FragColor = vec4(encodeFloatRGB(float(uObjectId)), 1.0);
         #elif defined(dRenderVariant_pickInstance)
@@ -116,12 +114,27 @@ void main() {
     #elif defined(dRenderVariant_depth)
         if (imageData.a < 0.05)
             discard;
-
         gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
+    #elif defined(dRenderVariant_marking)
+        float group = decodeFloatRGB(texture2D(tGroupTex, vUv).rgb);
+        float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+        #if defined(dRenderVariant_markingDepth)
+            if (vMarker > 0.0 || imageData.a < 0.05)
+                discard;
+            gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
+        #elif defined(dRenderVariant_markingMask)
+            if (vMarker == 0.0 || imageData.a < 0.05)
+                discard;
+            float depthTest = 1.0;
+            if (uMarkingDepthTest) {
+                depthTest = (fragmentDepth >= getDepth(gl_FragCoord.xy / uDrawingBufferSize)) ? 1.0 : 0.0;
+            }
+            bool isHighlight = intMod(floor(vMarker * 255.0 + 0.5), 2.0) > 0.1;
+            gl_FragColor = vec4(0.0, depthTest, isHighlight ? 1.0 : 0.0, 1.0);
+        #endif
     #elif defined(dRenderVariant_color)
         if (imageData.a < 0.05)
             discard;
-
         gl_FragColor = imageData;
         gl_FragColor.a *= uAlpha;
 

+ 3 - 2
src/mol-gl/shader/lines.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,7 +12,6 @@ precision highp int;
 #include common_frag_params
 #include color_frag_params
 #include common_clip
-#include wboit_params
 
 void main(){
     #include clip_pixel
@@ -26,6 +25,8 @@ void main(){
         gl_FragColor = material;
     #elif defined(dRenderVariant_depth)
         gl_FragColor = material;
+    #elif defined(dRenderVariant_marking)
+        gl_FragColor = material;
     #elif defined(dRenderVariant_color)
         gl_FragColor = material;
 

+ 28 - 0
src/mol-gl/shader/marking/edge.frag.ts

@@ -0,0 +1,28 @@
+export const edge_frag = `
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tMaskTexture;
+uniform vec2 uTexSizeInv;
+
+void main() {
+    vec2 coords = gl_FragCoord.xy * uTexSizeInv;
+    vec4 offset = vec4(float(dEdgeScale), 0.0, 0.0, float(dEdgeScale)) * vec4(uTexSizeInv, uTexSizeInv);
+    vec4 c0 = texture2D(tMaskTexture, coords);
+    vec4 c1 = texture2D(tMaskTexture, coords + offset.xy);
+    vec4 c2 = texture2D(tMaskTexture, coords - offset.xy);
+    vec4 c3 = texture2D(tMaskTexture, coords + offset.yw);
+    vec4 c4 = texture2D(tMaskTexture, coords - offset.yw);
+    float diff1 = (c1.r - c2.r) * 0.5;
+    float diff2 = (c3.r - c4.r) * 0.5;
+    float d = length(vec2(diff1, diff2));
+    if (d <= 0.0)
+        discard;
+    float a1 = min(c1.g, c2.g);
+    float a2 = min(c3.g, c4.g);
+    float visibility = min(a1, a2) > 0.001 ? 1.0 : 0.0;
+    float mask = c0.r;
+    float marker = min(c1.b, min(c2.b, min(c3.b, c4.b)));
+    gl_FragColor = vec4(visibility, mask, marker, 1.0);
+}
+`;

+ 23 - 0
src/mol-gl/shader/marking/overlay.frag.ts

@@ -0,0 +1,23 @@
+export const overlay_frag = `
+precision highp float;
+precision highp sampler2D;
+
+uniform vec2 uTexSizeInv;
+uniform sampler2D tEdgeTexture;
+uniform vec3 uHighlightEdgeColor;
+uniform vec3 uSelectEdgeColor;
+uniform float uGhostEdgeStrength;
+uniform float uInnerEdgeFactor;
+
+void main() {
+    vec2 coords = gl_FragCoord.xy * uTexSizeInv;
+    vec4 edgeValue = texture2D(tEdgeTexture, coords);
+    if (edgeValue.a > 0.0) {
+        vec3 edgeColor = edgeValue.b == 1.0 ? uHighlightEdgeColor : uSelectEdgeColor;
+        gl_FragColor.rgb = edgeValue.g > 0.0 ? edgeColor : edgeColor * uInnerEdgeFactor;
+        gl_FragColor.a = edgeValue.r == 1.0 ? uGhostEdgeStrength : 1.0;
+    } else {
+        gl_FragColor = vec4(0.0);
+    }
+}
+`;

+ 3 - 2
src/mol-gl/shader/mesh.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -14,7 +14,6 @@ precision highp int;
 #include light_frag_params
 #include normal_frag_params
 #include common_clip
-#include wboit_params
 
 void main() {
     #include clip_pixel
@@ -43,6 +42,8 @@ void main() {
         gl_FragColor = material;
     #elif defined(dRenderVariant_depth)
         gl_FragColor = material;
+    #elif defined(dRenderVariant_marking)
+        gl_FragColor = material;
     #elif defined(dRenderVariant_color)
         #ifdef dIgnoreLight
             gl_FragColor = material;

+ 3 - 2
src/mol-gl/shader/points.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,7 +12,6 @@ precision highp int;
 #include common_frag_params
 #include color_frag_params
 #include common_clip
-#include wboit_params
 
 #ifdef dPointFilledCircle
     uniform float uPointEdgeBleach;
@@ -33,6 +32,8 @@ void main(){
         gl_FragColor = material;
     #elif defined(dRenderVariant_depth)
         gl_FragColor = material;
+    #elif defined(dRenderVariant_marking)
+        gl_FragColor = material;
     #elif defined(dRenderVariant_color)
         gl_FragColor = material;
 

+ 3 - 2
src/mol-gl/shader/spheres.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -13,7 +13,6 @@ precision highp int;
 #include color_frag_params
 #include light_frag_params
 #include common_clip
-#include wboit_params
 
 varying float vRadius;
 varying float vRadiusSq;
@@ -86,6 +85,8 @@ void main(void){
         gl_FragColor = material;
     #elif defined(dRenderVariant_depth)
         gl_FragColor = material;
+    #elif defined(dRenderVariant_marking)
+        gl_FragColor = material;
     #elif defined(dRenderVariant_color)
         #ifdef dIgnoreLight
             gl_FragColor = material;

+ 3 - 2
src/mol-gl/shader/text.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,7 +12,6 @@ precision highp int;
 #include common_frag_params
 #include color_frag_params
 #include common_clip
-#include wboit_params
 
 uniform sampler2D tFont;
 
@@ -66,6 +65,8 @@ void main(){
         #include check_picking_alpha
     #elif defined(dRenderVariant_depth)
         gl_FragColor = material;
+    #elif defined(dRenderVariant_marking)
+        gl_FragColor = material;
     #elif defined(dRenderVariant_color)
         #include apply_marker_color
         #include apply_fog

+ 1 - 1
src/mol-gl/webgl/render-item.ts

@@ -49,7 +49,7 @@ export interface RenderItem<T extends string> {
 
 //
 
-const GraphicsRenderVariant = { 'colorBlended': '', 'colorWboit': '', 'pickObject': '', 'pickInstance': '', 'pickGroup': '', 'depth': '' };
+const GraphicsRenderVariant = { 'colorBlended': '', 'colorWboit': '', 'pickObject': '', 'pickInstance': '', 'pickGroup': '', 'depth': '', 'markingDepth': '', 'markingMask': '' };
 export type GraphicsRenderVariant = keyof typeof GraphicsRenderVariant
 const GraphicsRenderVariants = Object.keys(GraphicsRenderVariant) as GraphicsRenderVariant[];
 

+ 13 - 9
src/mol-plugin/util/viewport-screenshot.ts

@@ -1,11 +1,11 @@
-import { Viewport } from '../../mol-canvas3d/camera/util';
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
+import { Viewport } from '../../mol-canvas3d/camera/util';
 import { CameraHelperParams } from '../../mol-canvas3d/helper/camera-helper';
 import { ImagePass } from '../../mol-canvas3d/passes/image';
 import { canvasToBlob } from '../../mol-canvas3d/util';
@@ -108,8 +108,8 @@ class ViewportScreenshotHelper extends PluginComponent {
     private createPass(mutlisample: boolean) {
         const c = this.plugin.canvas3d!;
         const { colorBufferFloat, textureFloat } = c.webgl.extensions;
-        const aoProps = this.plugin.canvas3d!.props.postprocessing.occlusion;
-        return this.plugin.canvas3d!.getImagePass({
+        const aoProps = c.props.postprocessing.occlusion;
+        return c.getImagePass({
             transparentBackground: this.values.transparent,
             cameraHelper: { axes: this.values.axes },
             multiSample: {
@@ -121,7 +121,8 @@ class ViewportScreenshotHelper extends PluginComponent {
                 occlusion: aoProps.name === 'on'
                     ? { name: 'on', params: { ...aoProps.params, samples: 128 } }
                     : aoProps
-            }
+            },
+            marking: { ...c.props.marking }
         });
     }
 
@@ -133,17 +134,19 @@ class ViewportScreenshotHelper extends PluginComponent {
     private _imagePass: ImagePass;
     get imagePass() {
         if (this._imagePass) {
-            const aoProps = this.plugin.canvas3d!.props.postprocessing.occlusion;
+            const c = this.plugin.canvas3d!;
+            const aoProps = c.props.postprocessing.occlusion;
             this._imagePass.setProps({
                 cameraHelper: { axes: this.values.axes },
                 transparentBackground: this.values.transparent,
                 // TODO: optimize because this creates a copy of a large object!
                 postprocessing: {
-                    ...this.plugin.canvas3d!.props.postprocessing,
+                    ...c.props.postprocessing,
                     occlusion: aoProps.name === 'on'
                         ? { name: 'on', params: { ...aoProps.params, samples: 128 } }
                         : aoProps
-                }
+                },
+                marking: { ...c.props.marking }
             });
             return this._imagePass;
         }
@@ -266,7 +269,8 @@ class ViewportScreenshotHelper extends PluginComponent {
             cameraHelper: { axes: this.values.axes },
             transparentBackground: this.values.transparent,
             // TODO: optimize because this creates a copy of a large object!
-            postprocessing: canvasProps.postprocessing
+            postprocessing: canvasProps.postprocessing,
+            marking: canvasProps.marking
         });
         const imageData = this.previewPass.getImageData(w, h);
         const canvas = this.previewCanvas;

+ 20 - 4
src/mol-repr/visual.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -8,7 +8,7 @@ import { RuntimeContext } from '../mol-task';
 import { GraphicsRenderObject } from '../mol-gl/render-object';
 import { PickingId } from '../mol-geo/geometry/picking';
 import { Loci, isEmptyLoci, isEveryLoci } from '../mol-model/loci';
-import { MarkerAction, applyMarkerAction } from '../mol-util/marker-action';
+import { MarkerAction, applyMarkerAction, getMarkerInfo } from '../mol-util/marker-action';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { WebGLContext } from '../mol-gl/webgl/context';
 import { Theme } from '../mol-theme/theme';
@@ -23,6 +23,7 @@ import { Transparency } from '../mol-theme/transparency';
 import { createTransparency, clearTransparency, applyTransparencyValue, getTransparencyAverage } from '../mol-geo/geometry/transparency-data';
 import { Clipping } from '../mol-theme/clipping';
 import { createClipping, applyClippingGroups, clearClipping } from '../mol-geo/geometry/clipping-data';
+import { getMarkersAverage } from '../mol-geo/geometry/marker-data';
 
 export interface VisualContext {
     readonly runtime: RuntimeContext
@@ -70,17 +71,32 @@ namespace Visual {
     export function mark(renderObject: GraphicsRenderObject | undefined, loci: Loci, action: MarkerAction, lociApply: LociApply) {
         if (!renderObject) return false;
 
-        const { tMarker, uGroupCount, instanceCount } = renderObject.values;
+        const { tMarker, markerAverage, markerStatus, uGroupCount, instanceCount } = renderObject.values;
         const count = uGroupCount.ref.value * instanceCount.ref.value;
         const { array } = tMarker.ref.value;
 
         let changed = false;
+        let average = -1;
+        let status = -1;
         if (isEveryLoci(loci)) {
             changed = applyMarkerAction(array, Interval.ofLength(count), action);
+            if (changed) {
+                const info = getMarkerInfo(action, markerStatus.ref.value);
+                average = info.average;
+                status = info.status;
+            }
         } else if (!isEmptyLoci(loci)) {
             changed = lociApply(loci, interval => applyMarkerAction(array, interval, action), true);
         }
-        if (changed) ValueCell.update(tMarker, tMarker.ref.value);
+        if (changed) {
+            if (average === -1) {
+                average = getMarkersAverage(array, count);
+                if (average === 0) status = 0;
+            }
+            ValueCell.update(tMarker, tMarker.ref.value);
+            ValueCell.updateIfChanged(markerAverage, average);
+            ValueCell.updateIfChanged(markerStatus, status);
+        }
         return changed;
     }
 

+ 85 - 0
src/mol-util/marker-action.ts

@@ -120,3 +120,88 @@ export function applyMarkerAction(array: Uint8Array, set: OrderedSet, action: Ma
     }
     return true;
 }
+
+
+export interface MarkerInfo {
+    /**
+     * 0: none marked;
+     * 1: all marked;
+     * -1: unclear, need to be calculated
+     */
+    average: 0 | 1 | -1
+    /**
+     * 0: none marked;
+     * 1: all highlighted;
+     * 2: all selected;
+     * 3: all highlighted and selected
+     * -1: mixed/unclear
+     */
+    status: 0 | 1 | 2 | 3 | -1
+}
+
+export function getMarkerInfo(action: MarkerAction, currentStatus: number): MarkerInfo {
+    let average: MarkerInfo['average'] = -1;
+    let status: MarkerInfo['status'] = -1;
+    switch (action) {
+        case MarkerAction.Highlight:
+            if (currentStatus === 0 || currentStatus === 1) {
+                average = 1;
+                status = 1;
+            } else if (currentStatus === 2 || currentStatus === 3) {
+                average = 1;
+                status = 3;
+            } else {
+                average = 1;
+            }
+            break;
+        case MarkerAction.RemoveHighlight:
+            if (currentStatus === 0 || currentStatus === 1) {
+                average = 0;
+                status = 0;
+            } else if (currentStatus === 2 || currentStatus === 3) {
+                average = 1;
+                status = 2;
+            }
+            break;
+        case MarkerAction.Select:
+            if (currentStatus === 1 || currentStatus === 3) {
+                average = 1;
+                status = 3;
+            } else if (currentStatus === 0 || currentStatus === 2) {
+                average = 1;
+                status = 2;
+            } else {
+                average = 1;
+            }
+            break;
+        case MarkerAction.Deselect:
+            if (currentStatus === 1 || currentStatus === 3) {
+                average = 1;
+                status = 1;
+            } else if (currentStatus === 0 || currentStatus === 2) {
+                average = 0;
+                status = 0;
+            }
+            break;
+        case MarkerAction.Toggle:
+            if (currentStatus === 1) {
+                average = 1;
+                status = 3;
+            } else if (currentStatus === 2) {
+                average = 0;
+                status = 0;
+            } else if (currentStatus === 3) {
+                average = 1;
+                status = 1;
+            } else if (currentStatus === 0) {
+                average = 1;
+                status = 2;
+            }
+            break;
+        case MarkerAction.Clear:
+            average = 0;
+            status = 0;
+            break;
+    }
+    return { average, status };
+}