Browse Source

Merge pull request #258 from molstar/marking

Add optional marking pass
Alexander Rose 3 years ago
parent
commit
7d26567d40
39 changed files with 835 additions and 144 deletions
  1. 11 2
      CHANGELOG.md
  2. 5 4
      src/mol-canvas3d/canvas3d.ts
  3. 26 5
      src/mol-canvas3d/passes/draw.ts
  4. 3 1
      src/mol-canvas3d/passes/image.ts
  5. 194 0
      src/mol-canvas3d/passes/marking.ts
  6. 9 5
      src/mol-canvas3d/passes/multi-sample.ts
  7. 1 1
      src/mol-data/int/impl/ordered-set.ts
  8. 6 3
      src/mol-data/int/impl/sorted-array.ts
  9. 1 0
      src/mol-data/int/sorted-array.ts
  10. 32 1
      src/mol-geo/geometry/marker-data.ts
  11. 6 6
      src/mol-gl/_spec/renderer.spec.ts
  12. 7 1
      src/mol-gl/renderable/schema.ts
  13. 66 14
      src/mol-gl/renderer.ts
  14. 0 2
      src/mol-gl/shader-code.ts
  15. 4 4
      src/mol-gl/shader/chunks/apply-marker-color.glsl.ts
  16. 3 1
      src/mol-gl/shader/chunks/assign-marker-varying.glsl.ts
  17. 30 0
      src/mol-gl/shader/chunks/assign-material-color.glsl.ts
  18. 27 4
      src/mol-gl/shader/chunks/common-frag-params.glsl.ts
  19. 10 6
      src/mol-gl/shader/chunks/common-vert-params.glsl.ts
  20. 4 0
      src/mol-gl/shader/chunks/common.glsl.ts
  21. 0 20
      src/mol-gl/shader/chunks/wboit-params.glsl.ts
  22. 3 2
      src/mol-gl/shader/cylinders.frag.ts
  23. 37 11
      src/mol-gl/shader/direct-volume.frag.ts
  24. 30 7
      src/mol-gl/shader/image.frag.ts
  25. 3 2
      src/mol-gl/shader/lines.frag.ts
  26. 29 0
      src/mol-gl/shader/marking/edge.frag.ts
  27. 23 0
      src/mol-gl/shader/marking/overlay.frag.ts
  28. 3 2
      src/mol-gl/shader/mesh.frag.ts
  29. 3 2
      src/mol-gl/shader/points.frag.ts
  30. 3 2
      src/mol-gl/shader/spheres.frag.ts
  31. 3 2
      src/mol-gl/shader/text.frag.ts
  32. 1 1
      src/mol-gl/webgl/render-item.ts
  33. 18 12
      src/mol-model/structure/structure/element/loci.ts
  34. 13 9
      src/mol-plugin/util/viewport-screenshot.ts
  35. 2 1
      src/mol-repr/structure/complex-visual.ts
  36. 13 1
      src/mol-repr/structure/units-visual.ts
  37. 2 1
      src/mol-repr/structure/visual/util/element.ts
  38. 58 9
      src/mol-repr/visual.ts
  39. 146 0
      src/mol-util/marker-action.ts

+ 11 - 2
CHANGELOG.md

@@ -13,9 +13,18 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add ``Mesh`` processing helper ``.smoothEdges``
 - Smooth border of molecular-surface with ``includeParent`` enabled
 - Hide ``includeParent`` option from gaussian-surface visuals (not particularly useful)
-- 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``)
+- Improve marking performance
+    - Avoid superfluous calls to ``StructureElement.Loci.isWholeStructure``
+    - Check if loci is superset of visual
+    - Check if loci overlaps with unit visual
+    - Ensure ``Interval`` is used for ranges instead of ``SortedArray``
+    - Inline ``StructureElement.Loci.size`` code
+    - Add uniform marker type
+    - Special case for reversing previous mark
+- 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();

+ 1 - 1
src/mol-data/int/impl/ordered-set.ts

@@ -19,7 +19,7 @@ export const ofBounds = I.ofBounds;
 export function ofSortedArray(xs: Nums): OrderedSetImpl {
     if (!xs.length) return Empty;
     // check if the array is just a range
-    if (xs[xs.length - 1] - xs[0] + 1 === xs.length) return I.ofRange(xs[0], xs[xs.length - 1]);
+    if (S.isRange(xs)) return I.ofRange(xs[0], xs[xs.length - 1]);
     return xs as any;
 }
 

+ 6 - 3
src/mol-data/int/impl/sorted-array.ts

@@ -22,6 +22,7 @@ export function ofRange(min: number, max: number) {
     return ret;
 }
 export function is(xs: any): xs is Nums { return xs && (Array.isArray(xs) || !!xs.buffer); }
+export function isRange(xs: Nums) { return xs[xs.length - 1] - xs[0] + 1 === xs.length; }
 
 export function start(xs: Nums) { return xs[0]; }
 export function end(xs: Nums) { return xs[xs.length - 1] + 1;  }
@@ -59,9 +60,11 @@ export function getAt(xs: Nums, i: number) { return xs[i]; }
 
 export function areEqual(a: Nums, b: Nums) {
     if (a === b) return true;
-    const aSize = a.length;
+    let aSize = a.length;
     if (aSize !== b.length || a[0] !== b[0] || a[aSize - 1] !== b[aSize - 1]) return false;
-    for (let i = 0; i < aSize; i++) {
+    if (isRange(a)) return true;
+    aSize--;
+    for (let i = 1; i < aSize; i++) {
         if (a[i] !== b[i]) return false;
     }
     return true;
@@ -340,7 +343,7 @@ export function deduplicate(xs: Nums) {
 }
 
 export function indicesOf(a: Nums, b: Nums): Nums {
-    if (a === b) return ofSortedArray(createRangeArray(0, a.length - 1));
+    if (areEqual(a, b)) return ofSortedArray(createRangeArray(0, a.length - 1));
 
     const { startI: sI, startJ: sJ, endI, endJ } = getSuitableIntersectionRange(a, b);
     let i = sI, j = sJ;

+ 1 - 0
src/mol-data/int/sorted-array.ts

@@ -17,6 +17,7 @@ namespace SortedArray {
     /** create sorted array [min, max) (it does NOT contain the max value) */
     export const ofBounds: <T extends number = number>(min: T, max: T) => SortedArray<T> = (min, max) => Impl.ofRange(min, max - 1) as any;
     export const is: <T extends number = number>(v: any) => v is SortedArray<T> = Impl.is as any;
+    export const isRange: <T extends number = number>(array: ArrayLike<number>) => boolean = Impl.isRange as any;
 
     export const has: <T extends number = number>(array: SortedArray<T>, x: T) => boolean = Impl.has as any;
     /** Returns the index of `x` in `set` or -1 if not found. */

+ 32 - 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>
  */
@@ -9,20 +9,43 @@ import { Vec2 } from '../../mol-math/linear-algebra';
 import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
 
 export type MarkerData = {
+    uMarker: ValueCell<number>,
     tMarker: ValueCell<TextureImage<Uint8Array>>
     uMarkerTexDim: ValueCell<Vec2>
+    dMarkerType: ValueCell<string>,
+    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) {
+        sum += array[i] && 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.updateIfChanged(markerData.uMarker, 0);
         ValueCell.update(markerData.tMarker, markers);
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height));
+        ValueCell.updateIfChanged(markerData.dMarkerType, status === -1 ? 'groupInstance' : 'uniform');
+        ValueCell.updateIfChanged(markerData.markerAverage, average);
+        ValueCell.updateIfChanged(markerData.markerStatus, status);
         return markerData;
     } else {
         return {
+            uMarker: ValueCell.create(0),
             tMarker: ValueCell.create(markers),
             uMarkerTexDim: ValueCell.create(Vec2.create(markers.width, markers.height)),
+            markerAverage: ValueCell.create(average),
+            markerStatus: ValueCell.create(status),
+            dMarkerType: ValueCell.create('uniform'),
         };
     }
 }
@@ -30,13 +53,21 @@ export function createMarkers(count: number, markerData?: MarkerData): MarkerDat
 const emptyMarkerTexture = { array: new Uint8Array(1), width: 1, height: 1 };
 export function createEmptyMarkers(markerData?: MarkerData): MarkerData {
     if (markerData) {
+        ValueCell.updateIfChanged(markerData.uMarker, 0);
         ValueCell.update(markerData.tMarker, emptyMarkerTexture);
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(1, 1));
+        ValueCell.updateIfChanged(markerData.dMarkerType, 'uniform');
+        ValueCell.updateIfChanged(markerData.markerAverage, 0);
+        ValueCell.updateIfChanged(markerData.markerStatus, 0);
         return markerData;
     } else {
         return {
+            uMarker: ValueCell.create(0),
             tMarker: ValueCell.create(emptyMarkerTexture),
             uMarkerTexDim: ValueCell.create(Vec2.create(1, 1)),
+            markerAverage: ValueCell.create(0),
+            markerStatus: ValueCell.create(0),
+            dMarkerType: ValueCell.create('uniform'),
         };
     }
 }

+ 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);

+ 7 - 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>
@@ -208,8 +210,12 @@ export type SizeSchema = typeof SizeSchema
 export type SizeValues = Values<SizeSchema>
 
 export const MarkerSchema = {
+    uMarker: UniformSpec('f'),
     uMarkerTexDim: UniformSpec('v2'),
     tMarker: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+    dMarkerType: DefineSpec('string', ['uniform', 'groupInstance']),
+    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 - 4
src/mol-gl/shader/chunks/apply-marker-color.glsl.ts

@@ -1,11 +1,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
     }
 }
 `;

+ 3 - 1
src/mol-gl/shader/chunks/assign-marker-varying.glsl.ts

@@ -1,3 +1,5 @@
 export const assign_marker_varying = `
-vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+#if defined(dMarkerType_groupInstance)
+    vMarker = readFromTexture(tMarker, aInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+#endif
 `;

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

@@ -1,4 +1,13 @@
 export const assign_material_color = `
+#if defined(dRenderVariant_color) || defined(dRenderVariant_marking)
+    #if defined(dMarkerType_uniform)
+        float marker = uMarker;
+    #elif defined(dMarkerType_groupInstance)
+        float marker = vMarker;
+    #endif
+    marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
+#endif
+
 #if defined(dRenderVariant_color)
     #if defined(dUsePalette)
         vec4 material = vec4(texture2D(tPalette, vec2(vPaletteV, 0.5)).rgb, uAlpha);
@@ -20,6 +29,27 @@ export const assign_material_color = `
     #else
         vec4 material = packDepthToRGBA(gl_FragCoord.z);
     #endif
+#elif defined(dRenderVariant_markingDepth)
+    if (marker > 0.0)
+        discard;
+    #ifdef enabledFragDepth
+        vec4 material = packDepthToRGBA(gl_FragDepthEXT);
+    #else
+        vec4 material = packDepthToRGBA(gl_FragCoord.z);
+    #endif
+#elif defined(dRenderVariant_markingMask)
+    if (marker == 0.0)
+        discard;
+    float depthTest = 1.0;
+    if (uMarkingDepthTest) {
+        depthTest = (fragmentDepth >= getDepth(gl_FragCoord.xy / uDrawingBufferSize)) ? 1.0 : 0.0;
+    }
+    bool isHighlight = intMod(marker, 2.0) > 0.1;
+    float viewZ = depthToViewZ(uIsOrtho, fragmentDepth, uNear, uFar);
+    float fogFactor = smoothstep(uFogNear, uFogFar, abs(viewZ));
+    if (fogFactor == 1.0)
+        discard;
+    vec4 material = vec4(0.0, depthTest, isHighlight ? 1.0 : 0.0, 1.0 - fogFactor);
 #endif
 
 // apply screendoor transparency

+ 27 - 4
src/mol-gl/shader/chunks/common-frag-params.glsl.ts

@@ -21,10 +21,17 @@ uniform int uGroupCount;
 
 uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
-#if __VERSION__ == 100
-    varying float vMarker;
-#else
-    flat in float vMarker;
+uniform float uHighlightStrength;
+uniform float uSelectStrength;
+
+#if defined(dMarkerType_uniform)
+    uniform float uMarker;
+#elif defined(dMarkerType_groupInstance)
+    #if __VERSION__ == 100
+        varying float vMarker;
+    #else
+        flat in float vMarker;
+    #endif
 #endif
 
 varying vec3 vModelPosition;
@@ -52,4 +59,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;
+}
 `;

+ 10 - 6
src/mol-gl/shader/chunks/common-vert-params.glsl.ts

@@ -26,12 +26,16 @@ uniform vec4 uInvariantBoundingSphere;
     #endif
 #endif
 
-uniform vec2 uMarkerTexDim;
-uniform sampler2D tMarker;
-#if __VERSION__ == 100
-    varying float vMarker;
-#else
-    flat out float vMarker;
+#if defined(dMarkerType_uniform)
+    uniform float uMarker;
+#elif defined(dMarkerType_groupInstance)
+    uniform vec2 uMarkerTexDim;
+    uniform sampler2D tMarker;
+    #if __VERSION__ == 100
+        varying float vMarker;
+    #else
+        flat out float vMarker;
+    #endif
 #endif
 
 varying vec3 vModelPosition;

+ 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;

+ 37 - 11
src/mol-gl/shader/direct-volume.frag.ts

@@ -53,8 +53,15 @@ uniform int uGroupCount;
 
 uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
-uniform vec2 uMarkerTexDim;
-uniform sampler2D tMarker;
+uniform float uHighlightStrength;
+uniform float uSelectStrength;
+
+#if defined(dMarkerType_uniform)
+    uniform float uMarker;
+#elif defined(dMarkerType_groupInstance)
+    uniform vec2 uMarkerTexDim;
+    uniform sampler2D tMarker;
+#endif
 
 uniform float uFogNear;
 uniform float uFogFar;
@@ -69,6 +76,8 @@ uniform bool uInteriorColorFlag;
 uniform vec3 uInteriorColor;
 bool interior;
 
+uniform bool uRenderWboit;
+
 uniform float uNear;
 uniform float uFar;
 uniform float uIsOrtho;
@@ -122,7 +131,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));
@@ -322,7 +334,12 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
                         #include apply_light_color
                     #endif
 
-                    float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+                    #if defined(dMarkerType_uniform)
+                        float marker = uMarker;
+                    #elif defined(dMarkerType_groupInstance)
+                        float marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+                        marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
+                    #endif
                     #include apply_interior_color
                     #include apply_marker_color
 
@@ -385,14 +402,18 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
 
                 gl_FragColor.a = material.a * uAlpha * uTransferScale;
 
-                #ifdef dPackedGroup
-                    float group = decodeFloatRGB(textureGroup(floor(unitPos * uGridDim + 0.5) / uGridDim).rgb);
-                #else
-                    vec3 g = floor(unitPos * uGridDim + 0.5);
-                    float group = g.z + g.y * uGridDim.z + g.x * uGridDim.z * uGridDim.y;
+                #if defined(dMarkerType_uniform)
+                    float marker = uMarker;
+                #elif defined(dMarkerType_groupInstance)
+                    #ifdef dPackedGroup
+                        float group = decodeFloatRGB(textureGroup(floor(unitPos * uGridDim + 0.5) / uGridDim).rgb);
+                    #else
+                        vec3 g = floor(unitPos * uGridDim + 0.5);
+                        float group = g.z + g.y * uGridDim.z + g.x * uGridDim.z * uGridDim.y;
+                    #endif
+                    float marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+                    marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
                 #endif
-
-                float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
                 #include apply_marker_color
 
                 preFogAlphaBlended = (1.0 - preFogAlphaBlended) * gl_FragColor.a + preFogAlphaBlended;
@@ -432,6 +453,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

+ 30 - 7
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,17 +114,42 @@ void main() {
     #elif defined(dRenderVariant_depth)
         if (imageData.a < 0.05)
             discard;
-
         gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
+    #elif defined(dRenderVariant_marking)
+        #if defined(dMarkerType_uniform)
+            float marker = uMarker;
+        #elif defined(dMarkerType_groupInstance)
+            float group = decodeFloatRGB(texture2D(tGroupTex, vUv).rgb);
+            float marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+            marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
+        #endif
+        #if defined(dRenderVariant_markingDepth)
+            if (marker > 0.0 || imageData.a < 0.05)
+                discard;
+            gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
+        #elif defined(dRenderVariant_markingMask)
+            if (marker == 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(marker, 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;
 
-        float group = decodeFloatRGB(texture2D(tGroupTex, vUv).rgb);
-        float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+        #if defined(dMarkerType_uniform)
+            float marker = uMarker;
+        #elif defined(dMarkerType_groupInstance)
+            float group = decodeFloatRGB(texture2D(tGroupTex, vUv).rgb);
+            float marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+            marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
+        #endif
 
         #include apply_marker_color
         #include apply_fog

+ 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;
 

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

@@ -0,0 +1,29 @@
+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)));
+    float fogAlpha = min(c1.a, min(c2.a, min(c3.a, c4.a)));
+    gl_FragColor = vec4(visibility, mask, marker, fogAlpha);
+}
+`;

+ 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) * edgeValue.a;
+    } 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[];
 

+ 18 - 12
src/mol-model/structure/structure/element/loci.ts

@@ -66,7 +66,10 @@ export namespace Loci {
     }
 
     export function isEmpty(loci: Loci) {
-        return size(loci) === 0;
+        for (const u of loci.elements) {
+            if(OrderedSet.size(u.indices) > 0) return false;
+        }
+        return true;
     }
 
     export function isWholeStructure(loci: Loci) {
@@ -140,7 +143,7 @@ export namespace Loci {
         return Structure.create(units, { parent: loci.structure.parent });
     }
 
-    // TODO: there should be a version that property supports partitioned units
+    // TODO: there should be a version that properly supports partitioned units
     export function remap(loci: Loci, structure: Structure): Loci {
         if (structure === loci.structure) return loci;
 
@@ -250,6 +253,14 @@ export namespace Loci {
         return isSubset;
     }
 
+    function makeIndexSet(newIndices: ArrayLike<UnitIndex>): OrderedSet<UnitIndex> {
+        if (newIndices.length > 3 && SortedArray.isRange(newIndices)) {
+            return Interval.ofRange(newIndices[0], newIndices[newIndices.length - 1]);
+        } else {
+            return SortedArray.ofSortedArray(newIndices);
+        }
+    }
+
     export function extendToWholeResidues(loci: Loci, restrictToConformation?: boolean): Loci {
         const elements: Loci['elements'][0][] = [];
         const residueAltIds = new Set<string>();
@@ -294,7 +305,7 @@ export namespace Loci {
                     }
                 }
 
-                elements[elements.length] = { unit: lociElement.unit, indices: SortedArray.ofSortedArray(newIndices) };
+                elements[elements.length] = { unit: lociElement.unit, indices: makeIndexSet(newIndices) };
             } else {
                 // coarse elements are already by-residue
                 elements[elements.length] = lociElement;
@@ -316,14 +327,6 @@ export namespace Loci {
         return element.unit.elements.length === OrderedSet.size(element.indices);
     }
 
-    function makeIndexSet(newIndices: number[]): OrderedSet<UnitIndex> {
-        if (newIndices.length > 12 && newIndices[newIndices.length - 1] - newIndices[0] === newIndices.length - 1) {
-            return Interval.ofRange(newIndices[0], newIndices[newIndices.length - 1]);
-        } else {
-            return SortedArray.ofSortedArray(newIndices);
-        }
-    }
-
     function collectChains(unit: Unit, chainIndices: Set<ChainIndex>, elements: Loci['elements'][0][]) {
         const { index } = getChainSegments(unit);
         const xs = unit.elements;
@@ -467,7 +470,10 @@ export namespace Loci {
     }
 
     function getUnitIndices(elements: SortedArray<ElementIndex>, indices: SortedArray<ElementIndex>) {
-        return SortedArray.indicesOf<ElementIndex, UnitIndex>(elements, indices);
+        if (SortedArray.isRange(elements) && SortedArray.areEqual(elements, indices)) {
+            return Interval.ofLength(elements.length);
+        }
+        return makeIndexSet(SortedArray.indicesOf<ElementIndex, UnitIndex>(elements, indices));
     }
 
     export function extendToAllInstances(loci: Loci): Loci {

+ 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;

+ 2 - 1
src/mol-repr/structure/complex-visual.ts

@@ -67,6 +67,7 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
     const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, processValues, dispose } = builder;
     const { updateValues, updateBoundingSphere, updateRenderableState, createPositionIterator } = builder.geometryUtils;
     const updateState = VisualUpdateState.create();
+    const previousMark: Visual.PreviousMark = { loci: EmptyLoci, action: MarkerAction.None, status: -1 };
 
     let renderObject: GraphicsRenderObject<G['kind']> | undefined;
 
@@ -235,7 +236,7 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
             return renderObject ? getLoci(pickingId, currentStructure, renderObject.id) : EmptyLoci;
         },
         mark(loci: Loci, action: MarkerAction) {
-            return Visual.mark(renderObject, loci, action, lociApply);
+            return Visual.mark(renderObject, loci, action, lociApply, previousMark);
         },
         setVisibility(visible: boolean) {
             Visual.setVisibility(renderObject, visible);

+ 13 - 1
src/mol-repr/structure/units-visual.ts

@@ -71,6 +71,7 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
     const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, processValues, dispose } = builder;
     const { createEmpty: createEmptyGeometry, updateValues, updateBoundingSphere, updateRenderableState, createPositionIterator } = builder.geometryUtils;
     const updateState = VisualUpdateState.create();
+    const previousMark: Visual.PreviousMark = { loci: EmptyLoci, action: MarkerAction.None, status: -1 };
 
     let renderObject: GraphicsRenderObject<G['kind']> | undefined;
 
@@ -289,7 +290,18 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
             return renderObject ? getLoci(pickingId, currentStructureGroup, renderObject.id) : EmptyLoci;
         },
         mark(loci: Loci, action: MarkerAction) {
-            return Visual.mark(renderObject, loci, action, lociApply);
+            let hasInvariantId = true;
+            if (StructureElement.Loci.is(loci)) {
+                hasInvariantId = false;
+                const { invariantId } = currentStructureGroup.group.units[0];
+                for (const e of loci.elements) {
+                    if (e.unit.invariantId === invariantId) {
+                        hasInvariantId = true;
+                        break;
+                    }
+                }
+            }
+            return hasInvariantId ? Visual.mark(renderObject, loci, action, lociApply, previousMark) : false;
         },
         setVisibility(visible: boolean) {
             Visual.setVisibility(renderObject, visible);

+ 2 - 1
src/mol-repr/structure/visual/util/element.ts

@@ -164,8 +164,9 @@ export function eachElement(loci: Loci, structureGroup: StructureGroup, apply: (
     const { structure, group } = structureGroup;
     if (!Structure.areEquivalent(loci.structure, structure)) return false;
     const elementCount = group.elements.length;
+    const { unitIndexMap } = group;
     for (const e of loci.elements) {
-        const unitIdx = group.unitIndexMap.get(e.unit.id);
+        const unitIdx = unitIndexMap.get(e.unit.id);
         if (unitIdx !== undefined) {
             const offset = unitIdx * elementCount; // to target unit instance
             if (Interval.is(e.indices)) {

+ 58 - 9
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>
  */
@@ -7,8 +7,8 @@
 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 { Loci, isEmptyLoci, isEveryLoci, EveryLoci } from '../mol-model/loci';
+import { MarkerAction, applyMarkerAction, getMarkerInfo, setMarkerValue, getPartialMarkerAverage, MarkerActions, MarkerInfo } 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
@@ -67,20 +68,68 @@ namespace Visual {
         if (renderObject) renderObject.state.colorOnly = colorOnly;
     }
 
-    export function mark(renderObject: GraphicsRenderObject | undefined, loci: Loci, action: MarkerAction, lociApply: LociApply) {
-        if (!renderObject) return false;
+    export type PreviousMark = { loci: Loci, action: MarkerAction, status: MarkerInfo['status'] }
 
-        const { tMarker, uGroupCount, instanceCount } = renderObject.values;
+    export function mark(renderObject: GraphicsRenderObject | undefined, loci: Loci, action: MarkerAction, lociApply: LociApply, previous?: PreviousMark) {
+        if (!renderObject || isEmptyLoci(loci)) return false;
+
+        const { tMarker, dMarkerType, uMarker, markerAverage, markerStatus, uGroupCount, instanceCount } = renderObject.values;
         const count = uGroupCount.ref.value * instanceCount.ref.value;
         const { array } = tMarker.ref.value;
+        const currentStatus = markerStatus.ref.value as MarkerInfo['status'];
+
+        if (!isEveryLoci(loci)) {
+            let intervalSize = 0;
+            lociApply(loci, interval => {
+                intervalSize += Interval.size(interval);
+                return true;
+            }, true);
+            if (intervalSize === 0) return false;
+            if (intervalSize === count) loci = EveryLoci;
+        }
 
         let changed = false;
+        let average = -1;
+        let status: MarkerInfo['status'] = -1;
         if (isEveryLoci(loci)) {
-            changed = applyMarkerAction(array, Interval.ofLength(count), action);
-        } else if (!isEmptyLoci(loci)) {
+            const info = getMarkerInfo(action, currentStatus);
+            if (info.status !== -1) {
+                changed = currentStatus !== info.status;
+                if (changed) setMarkerValue(array, info.status, count);
+            } else {
+                changed = applyMarkerAction(array, Interval.ofLength(count), action);
+            }
+            average = info.average;
+            status = info.status;
+        } else {
             changed = lociApply(loci, interval => applyMarkerAction(array, interval, action), true);
+            if (changed) {
+                average = getPartialMarkerAverage(action, currentStatus);
+                if (previous && previous.status !== -1 && average === -1 &&
+                    MarkerActions.isReverse(previous.action, action) &&
+                    Loci.areEqual(loci, previous.loci)
+                ) {
+                    status = previous.status;
+                    average = status === 0 ? 0 : 0.5;
+                }
+            }
+        }
+        if (changed) {
+            if (average === -1) {
+                average = getMarkersAverage(array, count);
+                if (average === 0) status = 0;
+            }
+            if (previous) {
+                previous.action = action;
+                previous.loci = loci;
+                previous.status = currentStatus;
+            }
+            ValueCell.updateIfChanged(uMarker, status);
+            if (status === -1) ValueCell.update(tMarker, tMarker.ref.value);
+            ValueCell.updateIfChanged(dMarkerType, status === -1 ? 'groupInstance' : 'uniform');
+            ValueCell.updateIfChanged(markerAverage, average);
+            ValueCell.updateIfChanged(markerStatus, status);
         }
-        if (changed) ValueCell.update(tMarker, tMarker.ref.value);
         return changed;
     }
 

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

@@ -35,6 +35,20 @@ export namespace MarkerActions {
         MarkerAction.Select | MarkerAction.Deselect | MarkerAction.Toggle |
         MarkerAction.Clear
     ) as MarkerActions;
+
+    export function isReverse(a: MarkerAction, b: MarkerAction) {
+        return (
+            (a === MarkerAction.Highlight && b === MarkerAction.RemoveHighlight) ||
+            (a === MarkerAction.RemoveHighlight && b === MarkerAction.Highlight) ||
+            (a === MarkerAction.Select && b === MarkerAction.Deselect) ||
+            (a === MarkerAction.Deselect && b === MarkerAction.Select) ||
+            (a === MarkerAction.Toggle && b === MarkerAction.Toggle)
+        );
+    }
+}
+
+export function setMarkerValue(array: Uint8Array, status: 0 | 1 | 2 | 3, count: number) {
+    array.fill(status, 0, count);
 }
 
 export function applyMarkerActionAtPosition(array: Uint8Array, i: number, action: MarkerAction) {
@@ -120,3 +134,135 @@ 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: MarkerInfo['status']): 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 };
+}
+
+/**
+ * Assumes the action is applied to a partial set that is
+ * neither the empty set nor the full set.
+ */
+export function getPartialMarkerAverage(action: MarkerAction, currentStatus: MarkerInfo['status']) {
+    switch (action) {
+        case MarkerAction.Highlight:
+            return 0.5;
+        case MarkerAction.RemoveHighlight:
+            if (currentStatus === 0) {
+                return 0;
+            } else if (currentStatus === 2 || currentStatus === 3) {
+                return 0.5;
+            } else { // 1 | -1
+                return -1;
+            }
+        case MarkerAction.Select:
+            return 0.5;
+        case MarkerAction.Deselect:
+            if (currentStatus === 1 || currentStatus === 3) {
+                return 0.5;
+            } else if (currentStatus === 0) {
+                return 0;
+            } else { // 2 | -1
+                return -1;
+            }
+        case MarkerAction.Toggle:
+            if (currentStatus === -1) {
+                return -1;
+            } else { // 0 | 1 | 2 | 3
+                return 0.5;
+            }
+        case MarkerAction.Clear:
+            if (currentStatus === -1) {
+                return -1;
+            } else if (currentStatus === 0) {
+                return 0;
+            } else { // 1 | 2 | 3
+                return 0.5;
+            }
+        case MarkerAction.None:
+            return -1;
+        default:
+            assertUnreachable(action);
+    }
+}