Browse Source

Merge pull request #98 from AronKovacs/wboit

Wboit
David Sehnal 4 years ago
parent
commit
96f8ba5a80
52 changed files with 783 additions and 258 deletions
  1. 2 0
      src/apps/viewer/index.html
  2. 2 0
      src/apps/viewer/index.ts
  3. 3 3
      src/mol-canvas3d/canvas3d.ts
  4. 96 28
      src/mol-canvas3d/passes/draw.ts
  5. 2 2
      src/mol-canvas3d/passes/image.ts
  6. 12 14
      src/mol-canvas3d/passes/multi-sample.ts
  7. 2 2
      src/mol-canvas3d/passes/passes.ts
  8. 12 8
      src/mol-canvas3d/passes/pick.ts
  9. 10 5
      src/mol-canvas3d/passes/postprocessing.ts
  10. 120 0
      src/mol-canvas3d/passes/wboit.ts
  11. 12 0
      src/mol-geo/geometry/transparency-data.ts
  12. 5 5
      src/mol-gl/_spec/renderer.spec.ts
  13. 9 6
      src/mol-gl/renderable.ts
  14. 1 0
      src/mol-gl/renderable/direct-volume.ts
  15. 2 2
      src/mol-gl/renderable/image.ts
  16. 2 2
      src/mol-gl/renderable/lines.ts
  17. 2 2
      src/mol-gl/renderable/mesh.ts
  18. 2 2
      src/mol-gl/renderable/points.ts
  19. 3 0
      src/mol-gl/renderable/schema.ts
  20. 2 2
      src/mol-gl/renderable/spheres.ts
  21. 2 2
      src/mol-gl/renderable/text.ts
  22. 2 2
      src/mol-gl/renderable/texture-mesh.ts
  23. 169 72
      src/mol-gl/renderer.ts
  24. 14 15
      src/mol-gl/scene.ts
  25. 25 11
      src/mol-gl/shader-code.ts
  26. 1 1
      src/mol-gl/shader/chunks/apply-fog.glsl.ts
  27. 21 16
      src/mol-gl/shader/chunks/assign-material-color.glsl.ts
  28. 2 0
      src/mol-gl/shader/chunks/common-frag-params.glsl.ts
  29. 4 0
      src/mol-gl/shader/chunks/common.glsl.ts
  30. 20 0
      src/mol-gl/shader/chunks/wboit-params.glsl.ts
  31. 18 0
      src/mol-gl/shader/chunks/wboit-write.glsl.ts
  32. 30 9
      src/mol-gl/shader/direct-volume.frag.ts
  33. 17 0
      src/mol-gl/shader/evaluate-wboit.frag.ts
  34. 5 0
      src/mol-gl/shader/image.frag.ts
  35. 5 0
      src/mol-gl/shader/lines.frag.ts
  36. 4 0
      src/mol-gl/shader/mesh.frag.ts
  37. 5 0
      src/mol-gl/shader/points.frag.ts
  38. 4 8
      src/mol-gl/shader/spheres.frag.ts
  39. 5 0
      src/mol-gl/shader/text.frag.ts
  40. 9 4
      src/mol-gl/webgl/context.ts
  41. 0 1
      src/mol-gl/webgl/extensions.ts
  42. 14 2
      src/mol-gl/webgl/framebuffer.ts
  43. 6 12
      src/mol-gl/webgl/program.ts
  44. 22 8
      src/mol-gl/webgl/render-item.ts
  45. 21 2
      src/mol-gl/webgl/render-target.ts
  46. 33 0
      src/mol-gl/webgl/texture.ts
  47. 4 4
      src/mol-math/geometry/gaussian-density/gpu.ts
  48. 2 1
      src/mol-plugin/config.ts
  49. 2 1
      src/mol-plugin/context.ts
  50. 2 1
      src/mol-plugin/util/viewport-screenshot.ts
  51. 3 2
      src/mol-repr/visual.ts
  52. 11 1
      src/mol-util/array.ts

+ 2 - 0
src/apps/viewer/index.html

@@ -50,12 +50,14 @@
 
             var disableAntialiasing = getParam('disable-antialiasing', '[^&]+').trim() === '1';
             var pixelScale = parseFloat(getParam('pixel-scale', '[^&]+').trim() || '1');
+            var enableWboit = getParam('enable-wboit', '[^&]+').trim() === '1';
             var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
             var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase();
             var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase();
             var viewer = new molstar.Viewer('app', {
                 disableAntialiasing: disableAntialiasing,
                 pixelScale: pixelScale,
+                enableWboit: enableWboit,
                 layoutShowControls: !hideControls,
                 viewportShowExpand: false,
                 pdbProvider: pdbProvider || 'pdbe',

+ 2 - 0
src/apps/viewer/index.ts

@@ -68,6 +68,7 @@ const DefaultViewerOptions = {
     layoutShowLeftPanel: true,
     disableAntialiasing: false,
     pixelScale: 1,
+    enableWboit: false,
 
     viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -117,6 +118,7 @@ export class Viewer {
             config: [
                 [PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
                 [PluginConfig.General.PixelScale, o.pixelScale],
+                [PluginConfig.General.EnableWboit, o.enableWboit],
                 [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
                 [PluginConfig.Viewport.ShowControls, o.viewportShowControls],
                 [PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],

+ 3 - 3
src/mol-canvas3d/canvas3d.ts

@@ -149,7 +149,7 @@ namespace Canvas3D {
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, position?: Vec3 }
 
-    export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number }> = {}) {
+    export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number, enableWboit: boolean }> = {}) {
         const gl = getGLContext(canvas, {
             alpha: true,
             antialias: attribs.antialias ?? true,
@@ -301,7 +301,7 @@ namespace Canvas3D {
                 if (MultiSamplePass.isEnabled(p.multiSample)) {
                     multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
                 } else {
-                    const toDrawingBuffer = !PostprocessingPass.isEnabled(p.postprocessing) && scene.volumes.renderables.length === 0;
+                    const toDrawingBuffer = !PostprocessingPass.isEnabled(p.postprocessing) && scene.volumes.renderables.length === 0 && !passes.draw.wboitEnabled;
                     passes.draw.render(renderer, cam, scene, helper, toDrawingBuffer, p.transparentBackground);
                     if (!toDrawingBuffer) passes.postprocessing.render(cam, true, p.postprocessing);
                 }
@@ -667,7 +667,7 @@ namespace Canvas3D {
                 }
             },
             getImagePass: (props: Partial<ImageProps> = {}) => {
-                return new ImagePass(webgl, renderer, scene, camera, helper, props);
+                return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
             },
 
             get props() {

+ 96 - 28
src/mol-canvas3d/passes/draw.ts

@@ -2,10 +2,11 @@
  * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
  */
 
 import { WebGLContext } from '../../mol-gl/webgl/context';
-import { RenderTarget } from '../../mol-gl/webgl/render-target';
+import { createNullRenderTarget, RenderTarget } from '../../mol-gl/webgl/render-target';
 import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
 import { Texture } from '../../mol-gl/webgl/texture';
@@ -22,6 +23,7 @@ import { Helper } from '../helper/helper';
 import quad_vert from '../../mol-gl/shader/quad.vert';
 import depthMerge_frag from '../../mol-gl/shader/depth-merge.frag';
 import { StereoCamera } from '../camera/stereo';
+import { WboitPass } from './wboit';
 
 const DepthMergeSchema = {
     ...QuadSchema,
@@ -49,6 +51,8 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text
 }
 
 export class DrawPass {
+    private readonly drawTarget: RenderTarget
+
     readonly colorTarget: RenderTarget
     readonly depthTexture: Texture
     readonly depthTexturePrimitives: Texture
@@ -60,9 +64,17 @@ export class DrawPass {
     private depthTextureVolumes: Texture
     private depthMerge: DepthMergeRenderable
 
-    constructor(private webgl: WebGLContext, width: number, height: number) {
+    private wboit: WboitPass | undefined
+
+    get wboitEnabled() {
+        return !!this.wboit?.enabled;
+    }
+
+    constructor(private webgl: WebGLContext, width: number, height: number, enableWboit: boolean) {
         const { extensions, resources } = webgl;
 
+        this.drawTarget = createNullRenderTarget(webgl.gl);
+
         this.colorTarget = webgl.createRenderTarget(width, height);
         this.packedDepth = !extensions.depthTexture;
 
@@ -79,6 +91,8 @@ export class DrawPass {
             this.depthTextureVolumes.define(width, height);
         }
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
+
+        this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
     }
 
     setSize(width: number, height: number) {
@@ -102,13 +116,60 @@ export class DrawPass {
             }
 
             ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height));
+
+            if (this.wboit?.enabled) {
+                this.wboit.setSize(width, height);
+            }
         }
     }
 
-    _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {
-        const { x, y, width, height } = camera.viewport;
-        renderer.setViewport(x, y, width, height);
+    private _depthMerge() {
+        const { state, gl } = this.webgl;
+
+        this.depthMerge.update();
+        this.depthTarget.bind();
+        state.disable(gl.BLEND);
+        state.disable(gl.DEPTH_TEST);
+        state.disable(gl.CULL_FACE);
+        state.depthMask(false);
+        state.clearColor(1, 1, 1, 1);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+        this.depthMerge.render();
+    }
+
+    private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean) {
+        if (!this.wboit?.enabled) throw new Error('expected wboit to be enabled');
+
+        const renderTarget = toDrawingBuffer ? this.drawTarget : this.colorTarget;
+        renderTarget.bind();
+        renderer.clear(true);
 
+        // render opaque primitives
+        this.depthTexturePrimitives.attachFramebuffer(renderTarget.framebuffer, 'depth');
+        renderTarget.bind();
+        renderer.renderWboitOpaque(scene.primitives, camera, null);
+
+        // render opaque volumes
+        this.depthTextureVolumes.attachFramebuffer(renderTarget.framebuffer, 'depth');
+        renderTarget.bind();
+        renderer.clearDepth();
+        renderer.renderWboitOpaque(scene.volumes, camera, this.depthTexturePrimitives);
+
+        // merge depth of opaque primitives and volumes
+        this._depthMerge();
+
+        // render transparent primitives and volumes
+        this.wboit.bind();
+        renderer.renderWboitTransparent(scene.primitives, camera, this.depthTexture);
+        renderer.renderWboitTransparent(scene.volumes, camera, this.depthTexture);
+
+        // evaluate wboit
+        this.depthTexturePrimitives.attachFramebuffer(renderTarget.framebuffer, 'depth');
+        renderTarget.bind();
+        this.wboit.render();
+    }
+
+    private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean) {
         if (toDrawingBuffer) {
             this.webgl.unbindFramebuffer();
         } else {
@@ -118,13 +179,16 @@ export class DrawPass {
             }
         }
 
-        renderer.render(scene.primitives, camera, 'color', true, transparentBackground, 1, null);
+        renderer.clear(true);
+        // TODO: split into opaque and transparent pass to handle opaque volume isosurfaces
+        renderer.renderBlended(scene.primitives, camera, null);
 
         // do a depth pass if not rendering to drawing buffer and
         // extensions.depthTexture is unsupported (i.e. depthTarget is set)
         if (!toDrawingBuffer && this.depthTargetPrimitives) {
             this.depthTargetPrimitives.bind();
-            renderer.render(scene.primitives, camera, 'depth', true, transparentBackground, 1, null);
+            renderer.clear(false);
+            renderer.renderDepth(scene.primitives, camera, null);
             this.colorTarget.bind();
         }
 
@@ -132,49 +196,53 @@ export class DrawPass {
         if (!toDrawingBuffer) {
             if (!this.packedDepth) {
                 this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
-                this.webgl.state.depthMask(true);
-                this.webgl.gl.viewport(x, y, width, height);
-                this.webgl.gl.scissor(x, y, width, height);
-                this.webgl.gl.clear(this.webgl.gl.DEPTH_BUFFER_BIT);
+                renderer.clearDepth();
             }
-            renderer.render(scene.volumes, camera, 'color', false, transparentBackground, 1, this.depthTexturePrimitives);
+            renderer.renderBlended(scene.volumes, camera, this.depthTexturePrimitives);
 
             // do volume depth pass if extensions.depthTexture is unsupported (i.e. depthTarget is set)
             if (this.depthTargetVolumes) {
                 this.depthTargetVolumes.bind();
-                renderer.render(scene.volumes, camera, 'depth', true, transparentBackground, 1, this.depthTexturePrimitives);
+                renderer.clear(false);
+                renderer.renderDepth(scene.volumes, camera, this.depthTexturePrimitives);
                 this.colorTarget.bind();
             }
         }
 
         // merge depths from primitive and volume rendering
         if (!toDrawingBuffer) {
-            this.depthMerge.update();
-            this.depthTarget.bind();
-            // this.webgl.state.disable(this.webgl.gl.SCISSOR_TEST);
-            this.webgl.state.disable(this.webgl.gl.BLEND);
-            this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
-            this.webgl.state.disable(this.webgl.gl.CULL_FACE);
-            this.webgl.state.depthMask(false);
-            this.webgl.state.clearColor(1, 1, 1, 1);
-            this.webgl.gl.viewport(x, y, width, height);
-            this.webgl.gl.scissor(x, y, width, height);
-            this.webgl.gl.clear(this.webgl.gl.COLOR_BUFFER_BIT);
-            this.depthMerge.render();
+            this._depthMerge();
             this.colorTarget.bind();
         }
+    }
+
+    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {
+        const { x, y, width, height } = camera.viewport;
+        renderer.setViewport(x, y, width, height);
+        renderer.setTransparentBackground(transparentBackground);
+        renderer.setDrawingBufferScale(1);
+        renderer.update(camera);
+
+        if (this.wboitEnabled) {
+            this._renderWboit(renderer, camera, scene, toDrawingBuffer);
+        } else {
+            this._renderBlended(renderer, camera, scene, toDrawingBuffer);
+        }
 
         if (helper.debug.isEnabled) {
             helper.debug.syncVisibility();
-            renderer.render(helper.debug.scene, camera, 'color', false, transparentBackground, 1, null);
+            renderer.renderBlended(helper.debug.scene, camera, null);
         }
         if (helper.handle.isEnabled) {
-            renderer.render(helper.handle.scene, camera, 'color', false, transparentBackground, 1, null);
+            renderer.renderBlended(helper.handle.scene, camera, null);
         }
         if (helper.camera.isEnabled) {
             helper.camera.update(camera);
-            renderer.render(helper.camera.scene, helper.camera.camera, 'color', false, transparentBackground, 1, null);
+            renderer.update(helper.camera.camera);
+            renderer.renderBlended(helper.camera.scene, helper.camera.camera, null);
         }
+
+        this.webgl.gl.flush();
     }
 
     render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {

+ 2 - 2
src/mol-canvas3d/passes/image.ts

@@ -46,10 +46,10 @@ export class ImagePass {
     get width() { return this._width; }
     get height() { return this._height; }
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, props: Partial<ImageProps>) {
+    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) {
         this.props = { ...PD.getDefaultValues(ImageParams), ...props };
 
-        this.drawPass = new DrawPass(webgl, 128, 128);
+        this.drawPass = new DrawPass(webgl, 128, 128, enableWboit);
         this.postprocessingPass = new PostprocessingPass(webgl, this.drawPass);
         this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass, this.postprocessingPass);
         this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);

+ 12 - 14
src/mol-canvas3d/passes/multi-sample.ts

@@ -69,11 +69,11 @@ export class MultiSamplePass {
     private compose: ComposeRenderable
 
     constructor(private webgl: WebGLContext, private drawPass: DrawPass, private postprocessing: PostprocessingPass) {
-        const { extensions } = webgl;
+        const { colorBufferFloat, textureFloat } = webgl.extensions;
         const width = drawPass.colorTarget.getWidth();
         const height = drawPass.colorTarget.getHeight();
         this.colorTarget = webgl.createRenderTarget(width, height, false);
-        this.composeTarget = webgl.createRenderTarget(width, height, false, extensions.colorBufferFloat ? 'float32' : 'uint8');
+        this.composeTarget = webgl.createRenderTarget(width, height, false, colorBufferFloat && textureFloat ? 'float32' : 'uint8');
         this.holdTarget = webgl.createRenderTarget(width, height, false);
         this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture);
     }
@@ -100,6 +100,14 @@ export class MultiSamplePass {
         }
     }
 
+    private bindOutputTarget(toDrawingBuffer: boolean) {
+        if (toDrawingBuffer) {
+            this.webgl.unbindFramebuffer();
+        } else {
+            this.colorTarget.bind();
+        }
+    }
+
     private renderMultiSample(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
         const { compose, composeTarget, drawPass, postprocessing, webgl } = this;
         const { gl, state } = webgl;
@@ -159,12 +167,7 @@ export class MultiSamplePass {
         ValueCell.update(compose.values.tColor, composeTarget.texture);
         compose.update();
 
-        if (toDrawingBuffer) {
-            webgl.unbindFramebuffer();
-        } else {
-            this.colorTarget.bind();
-        }
-
+        this.bindOutputTarget(toDrawingBuffer);
         gl.viewport(x, y, width, height);
         gl.scissor(x, y, width, height);
 
@@ -245,12 +248,7 @@ export class MultiSamplePass {
             }
         }
 
-        if (toDrawingBuffer) {
-            webgl.unbindFramebuffer();
-        } else {
-            this.colorTarget.bind();
-        }
-
+        this.bindOutputTarget(toDrawingBuffer);
         gl.viewport(x, y, width, height);
         gl.scissor(x, y, width, height);
 

+ 2 - 2
src/mol-canvas3d/passes/passes.ts

@@ -16,9 +16,9 @@ export class Passes {
     readonly postprocessing: PostprocessingPass
     readonly multiSample: MultiSamplePass
 
-    constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number }> = {}) {
+    constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) {
         const { gl } = webgl;
-        this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight);
+        this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false);
         this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
         this.postprocessing = new PostprocessingPass(webgl, this.draw);
         this.multiSample = new MultiSamplePass(webgl, this.draw, this.postprocessing);

+ 12 - 8
src/mol-canvas3d/passes/pick.ts

@@ -64,14 +64,16 @@ export class PickPass {
     }
 
     private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: GraphicsRenderVariant) {
-        const pickScale = this.pickBaseScale / this.webgl.pixelRatio;
         const depth = this.drawPass.depthTexturePrimitives;
-        renderer.render(scene.primitives, camera, variant, true, false, pickScale, null);
-        renderer.render(scene.volumes, camera, variant, false, false, pickScale, depth);
-        renderer.render(helper.handle.scene, camera, variant, false, false, pickScale, null);
+        renderer.clear(false);
+        renderer.renderPick(scene.primitives, camera, variant, null);
+        renderer.renderPick(scene.volumes, camera, variant, depth);
+        renderer.renderPick(helper.handle.scene, camera, variant, null);
     }
 
     render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper) {
+        renderer.update(camera);
+
         this.objectPickTarget.bind();
         this.renderVariant(renderer, camera, scene, helper, 'pickObject');
 
@@ -165,17 +167,19 @@ export class PickHelper {
 
     private render(camera: Camera | StereoCamera) {
         const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
-
         const { renderer, scene, helper } = this;
 
+        renderer.setTransparentBackground(false);
+        renderer.setDrawingBufferScale(this.pickScale);
+
         if (StereoCamera.is(camera)) {
-            this.renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
+            renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
             this.pickPass.render(renderer, camera.left, scene, helper);
 
-            this.renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
+            renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
             this.pickPass.render(renderer, camera.right, scene, helper);
         } else {
-            this.renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
+            renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
             this.pickPass.render(renderer, camera, scene, helper);
         }
 

+ 10 - 5
src/mol-canvas3d/passes/postprocessing.ts

@@ -110,6 +110,14 @@ export class PostprocessingPass {
         this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture);
     }
 
+    private bindTarget(toDrawingBuffer: boolean) {
+        if (toDrawingBuffer) {
+            this.webgl.unbindFramebuffer();
+        } else {
+            this.target.bind();
+        }
+    }
+
     syncSize() {
         const width = this.drawPass.colorTarget.getWidth();
         const height = this.drawPass.colorTarget.getHeight();
@@ -159,11 +167,8 @@ export class PostprocessingPass {
         }
 
         const { gl, state } = this.webgl;
-        if (toDrawingBuffer) {
-            this.webgl.unbindFramebuffer();
-        } else {
-            this.target.bind();
-        }
+        this.bindTarget(toDrawingBuffer);
+
         state.disable(gl.SCISSOR_TEST);
         state.disable(gl.BLEND);
         state.disable(gl.DEPTH_TEST);

+ 120 - 0
src/mol-canvas3d/passes/wboit.ts

@@ -0,0 +1,120 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { 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 { ValueCell } from '../../mol-util';
+import quad_vert from '../../mol-gl/shader/quad.vert';
+import evaluate_wboit_frag from '../../mol-gl/shader/evaluate-wboit.frag';
+import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
+import { Vec2 } from '../../mol-math/linear-algebra';
+import { isDebugMode } from '../../mol-util/debug';
+
+const EvaluateWboitSchema = {
+    ...QuadSchema,
+    tWboitA: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tWboitB: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+};
+const EvaluateWboitShaderCode = ShaderCode('evaluate-wboit', quad_vert, evaluate_wboit_frag);
+type EvaluateWboitRenderable = ComputeRenderable<Values<typeof EvaluateWboitSchema>>
+
+function getEvaluateWboitRenderable(ctx: WebGLContext, wboitATexture: Texture, wboitBTexture: Texture): EvaluateWboitRenderable {
+    const values: Values<typeof EvaluateWboitSchema> = {
+        ...QuadValues,
+        tWboitA: ValueCell.create(wboitATexture),
+        tWboitB: ValueCell.create(wboitBTexture),
+        uTexSize: ValueCell.create(Vec2.create(wboitATexture.getWidth(), wboitATexture.getHeight())),
+    };
+
+    const schema = { ...EvaluateWboitSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', EvaluateWboitShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+//
+
+export class WboitPass {
+    private readonly renderable: EvaluateWboitRenderable
+
+    private readonly framebuffer: Framebuffer
+    private readonly textureA: Texture
+    private readonly textureB: Texture
+
+    private _enabled = false;
+    get enabled() {
+        return this._enabled;
+    }
+
+    bind() {
+        const { state, gl } = this.webgl;
+
+        this.framebuffer.bind();
+
+        state.clearColor(0, 0, 0, 1);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        state.disable(gl.DEPTH_TEST);
+
+        state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ZERO, gl.ONE_MINUS_SRC_ALPHA);
+        state.enable(gl.BLEND);
+    }
+
+    render() {
+        const { state, gl } = this.webgl;
+
+        state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
+        state.enable(gl.BLEND);
+
+        this.renderable.update();
+        this.renderable.render();
+    }
+
+    setSize(width: number, height: number) {
+        const [w, h] = this.renderable.values.uTexSize.ref.value;
+        if (width !== w || height !== h) {
+            this.textureA.define(width, height);
+            this.textureB.define(width, height);
+            ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
+        }
+    }
+
+    constructor(private webgl: WebGLContext, width: number, height: number) {
+        const { resources, extensions } = webgl;
+        const { drawBuffers, textureFloat, colorBufferFloat, depthTexture } = extensions;
+        if (!textureFloat || !colorBufferFloat || !depthTexture || !drawBuffers) {
+            if (isDebugMode) console.log('Missing extensions required for "wboit"');
+            return;
+        }
+
+        this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        this.textureA.define(width, height);
+
+        this.textureB = resources.texture('image-float32', 'rgba', 'float', 'nearest');
+        this.textureB.define(width, height);
+
+        this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
+
+        this.framebuffer = resources.framebuffer();
+        this.framebuffer.bind();
+        drawBuffers.drawBuffers([
+            drawBuffers.COLOR_ATTACHMENT0,
+            drawBuffers.COLOR_ATTACHMENT1,
+        ]);
+
+        this.textureA.attachFramebuffer(this.framebuffer, 'color0');
+        this.textureB.attachFramebuffer(this.framebuffer, 'color1');
+
+        this._enabled = true;
+    }
+}

+ 12 - 0
src/mol-geo/geometry/transparency-data.ts

@@ -13,6 +13,7 @@ export type TransparencyData = {
     uTransparencyTexDim: ValueCell<Vec2>
     dTransparency: ValueCell<boolean>,
     dTransparencyVariant: ValueCell<string>,
+    transparencyAverage: ValueCell<number>,
 }
 
 export function applyTransparencyValue(array: Uint8Array, start: number, end: number, value: number) {
@@ -22,6 +23,14 @@ export function applyTransparencyValue(array: Uint8Array, start: number, end: nu
     return true;
 }
 
+export function getTransparencyAverage(array: Uint8Array, count: number): number {
+    let sum = 0;
+    for (let i = 0; i < count; ++i) {
+        sum += array[i];
+    }
+    return sum / (255 * count);
+}
+
 export function clearTransparency(array: Uint8Array, start: number, end: number) {
     array.fill(0, start, end);
 }
@@ -32,6 +41,7 @@ export function createTransparency(count: number, transparencyData?: Transparenc
         ValueCell.update(transparencyData.tTransparency, transparency);
         ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height));
         ValueCell.updateIfChanged(transparencyData.dTransparency, count > 0);
+        ValueCell.updateIfChanged(transparencyData.transparencyAverage, getTransparencyAverage(transparency.array, count));
         return transparencyData;
     } else {
         return {
@@ -39,6 +49,7 @@ export function createTransparency(count: number, transparencyData?: Transparenc
             uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)),
             dTransparency: ValueCell.create(count > 0),
             dTransparencyVariant: ValueCell.create('single'),
+            transparencyAverage: ValueCell.create(0),
         };
     }
 }
@@ -55,6 +66,7 @@ export function createEmptyTransparency(transparencyData?: TransparencyData): Tr
             uTransparencyTexDim: ValueCell.create(Vec2.create(1, 1)),
             dTransparency: ValueCell.create(false),
             dTransparencyVariant: ValueCell.create('single'),
+            transparencyAverage: ValueCell.create(0),
         };
     }
 }

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

@@ -132,17 +132,17 @@ describe('renderer', () => {
         scene.commit();
         expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
         expect(ctx.stats.resourceCounts.texture).toBe(6);
-        expect(ctx.stats.resourceCounts.vertexArray).toBe(5);
-        expect(ctx.stats.resourceCounts.program).toBe(5);
-        expect(ctx.stats.resourceCounts.shader).toBe(10);
+        expect(ctx.stats.resourceCounts.vertexArray).toBe(6);
+        expect(ctx.stats.resourceCounts.program).toBe(6);
+        expect(ctx.stats.resourceCounts.shader).toBe(12);
 
         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(5);
-        expect(ctx.stats.resourceCounts.shader).toBe(10);
+        expect(ctx.stats.resourceCounts.program).toBe(6);
+        expect(ctx.stats.resourceCounts.shader).toBe(12);
 
         ctx.resources.destroy();
         expect(ctx.stats.resourceCounts.program).toBe(0);

+ 9 - 6
src/mol-gl/renderable.ts

@@ -1,15 +1,16 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Program } from './webgl/program';
-import { RenderableValues, Values, RenderableSchema } from './renderable/schema';
+import { RenderableValues, Values, RenderableSchema, BaseValues } from './renderable/schema';
 import { GraphicsRenderItem, ComputeRenderItem, GraphicsRenderVariant } from './webgl/render-item';
 import { ValueCell } from '../mol-util';
 import { idFactory } from '../mol-util/id-factory';
 import { clamp } from '../mol-math/interpolate';
+import { Textures } from './webgl/texture';
 
 const getNextRenderableId = idFactory();
 
@@ -19,7 +20,7 @@ export type RenderableState = {
     pickable: boolean
     colorOnly: boolean
     opaque: boolean
-    writeDepth: boolean,
+    writeDepth: boolean
 }
 
 export interface Renderable<T extends RenderableValues> {
@@ -28,7 +29,7 @@ export interface Renderable<T extends RenderableValues> {
     readonly values: T
     readonly state: RenderableState
 
-    render: (variant: GraphicsRenderVariant) => void
+    render: (variant: GraphicsRenderVariant, sharedTexturesList?: Textures) => void
     getProgram: (variant: GraphicsRenderVariant) => Program
     update: () => void
     dispose: () => void
@@ -41,11 +42,11 @@ export function createRenderable<T extends Values<RenderableSchema>>(renderItem:
         values,
         state,
 
-        render: (variant: GraphicsRenderVariant) => {
+        render: (variant: GraphicsRenderVariant, sharedTexturesList?: Textures) => {
             if (values.uAlpha && values.alpha) {
                 ValueCell.updateIfChanged(values.uAlpha, clamp(values.alpha.ref.value * state.alphaFactor, 0, 1));
             }
-            renderItem.render(variant);
+            renderItem.render(variant, sharedTexturesList);
         },
         getProgram: (variant: GraphicsRenderVariant) => renderItem.getProgram(variant),
         update: () => renderItem.update(),
@@ -53,6 +54,8 @@ export function createRenderable<T extends Values<RenderableSchema>>(renderItem:
     };
 }
 
+export type GraphicsRenderable = Renderable<RenderableValues & BaseValues>
+
 //
 
 export interface ComputeRenderable<T extends RenderableValues> {

+ 1 - 0
src/mol-gl/renderable/direct-volume.ts

@@ -28,6 +28,7 @@ export const DirectVolumeSchema = {
     tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     dTransparency: DefineSpec('boolean'),
     dTransparencyVariant: DefineSpec('string', ['single', 'multi']),
+    transparencyAverage: ValueSpec('number'),
 
     dClipObjectCount: DefineSpec('number'),
     dClipVariant: DefineSpec('string', ['instance', 'pixel']),

+ 2 - 2
src/mol-gl/renderable/image.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { AttributeSpec, Values, GlobalUniformSchema, InternalSchema, TextureSpec, ElementsSpec, DefineSpec, InternalValues, BaseSchema, UniformSpec } from './schema';
+import { AttributeSpec, Values, GlobalUniformSchema, InternalSchema, TextureSpec, ElementsSpec, DefineSpec, InternalValues, BaseSchema, UniformSpec, GlobalTextureSchema } from './schema';
 import { ImageShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 import { InterpolationTypeNames } from '../../mol-geo/geometry/image/image';
@@ -30,7 +30,7 @@ export type ImageSchema = typeof ImageSchema
 export type ImageValues = Values<ImageSchema>
 
 export function ImageRenderable(ctx: WebGLContext, id: number, values: ImageValues, state: RenderableState, materialId: number): Renderable<ImageValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...ImageSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...ImageSchema };
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
     };

+ 2 - 2
src/mol-gl/renderable/lines.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues } from './schema';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema } from './schema';
 import { ValueCell } from '../../mol-util';
 import { LinesShaderCode } from '../shader-code';
 
@@ -26,7 +26,7 @@ export type LinesSchema = typeof LinesSchema
 export type LinesValues = Values<LinesSchema>
 
 export function LinesRenderable(ctx: WebGLContext, id: number, values: LinesValues, state: RenderableState, materialId: number): Renderable<LinesValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...LinesSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...LinesSchema };
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
     };

+ 2 - 2
src/mol-gl/renderable/mesh.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues } from './schema';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema } from './schema';
 import { MeshShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 
@@ -25,7 +25,7 @@ export type MeshSchema = typeof MeshSchema
 export type MeshValues = Values<MeshSchema>
 
 export function MeshRenderable(ctx: WebGLContext, id: number, values: MeshValues, state: RenderableState, materialId: number): Renderable<MeshValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...MeshSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...MeshSchema };
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
     };

+ 2 - 2
src/mol-gl/renderable/points.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues } from './schema';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema } from './schema';
 import { PointsShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 
@@ -23,7 +23,7 @@ export type PointsSchema = typeof PointsSchema
 export type PointsValues = Values<PointsSchema>
 
 export function PointsRenderable(ctx: WebGLContext, id: number, values: PointsValues, state: RenderableState, materialId: number): Renderable<PointsValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...PointsSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...PointsSchema };
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
     };

+ 3 - 0
src/mol-gl/renderable/schema.ts

@@ -155,6 +155,8 @@ export const GlobalUniformSchema = {
 
     uHighlightColor: UniformSpec('v3'),
     uSelectColor: UniformSpec('v3'),
+
+    uRenderWboit: UniformSpec('i'),
 } as const;
 export type GlobalUniformSchema = typeof GlobalUniformSchema
 export type GlobalUniformValues = Values<GlobalUniformSchema>
@@ -212,6 +214,7 @@ export const TransparencySchema = {
     tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
     dTransparency: DefineSpec('boolean'),
     dTransparencyVariant: DefineSpec('string', ['single', 'multi']),
+    transparencyAverage: ValueSpec('number'),
 } as const;
 export type TransparencySchema = typeof TransparencySchema
 export type TransparencyValues = Values<TransparencySchema>

+ 2 - 2
src/mol-gl/renderable/spheres.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec } from './schema';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema } from './schema';
 import { SpheresShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 
@@ -26,7 +26,7 @@ export type SpheresSchema = typeof SpheresSchema
 export type SpheresValues = Values<SpheresSchema>
 
 export function SpheresRenderable(ctx: WebGLContext, id: number, values: SpheresValues, state: RenderableState, materialId: number): Renderable<SpheresValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...SpheresSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...SpheresSchema };
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
     };

+ 2 - 2
src/mol-gl/renderable/text.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, Values, InternalSchema, SizeSchema, InternalValues, TextureSpec, ElementsSpec, ValueSpec } from './schema';
+import { GlobalUniformSchema, BaseSchema, AttributeSpec, UniformSpec, Values, InternalSchema, SizeSchema, InternalValues, TextureSpec, ElementsSpec, ValueSpec, GlobalTextureSchema } from './schema';
 import { TextShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 
@@ -35,7 +35,7 @@ export type TextSchema = typeof TextSchema
 export type TextValues = Values<TextSchema>
 
 export function TextRenderable(ctx: WebGLContext, id: number, values: TextValues, state: RenderableState, materialId: number): Renderable<TextValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...TextSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...TextSchema };
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
     };

+ 2 - 2
src/mol-gl/renderable/texture-mesh.ts

@@ -7,7 +7,7 @@
 import { Renderable, RenderableState, createRenderable } from '../renderable';
 import { WebGLContext } from '../webgl/context';
 import { createGraphicsRenderItem } from '../webgl/render-item';
-import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec } from './schema';
+import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema } from './schema';
 import { MeshShaderCode } from '../shader-code';
 import { ValueCell } from '../../mol-util';
 
@@ -28,7 +28,7 @@ export type TextureMeshSchema = typeof TextureMeshSchema
 export type TextureMeshValues = Values<TextureMeshSchema>
 
 export function TextureMeshRenderable(ctx: WebGLContext, id: number, values: TextureMeshValues, state: RenderableState, materialId: number): Renderable<TextureMeshValues> {
-    const schema = { ...GlobalUniformSchema, ...InternalSchema, ...TextureMeshSchema };
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...TextureMeshSchema };
     const internalValues: InternalValues = {
         uObjectId: ValueCell.create(id),
     };

+ 169 - 72
src/mol-gl/renderer.ts

@@ -9,17 +9,18 @@ import { ICamera } from '../mol-canvas3d/camera';
 import Scene from './scene';
 import { WebGLContext } from './webgl/context';
 import { Mat4, Vec3, Vec4, Vec2, Quat } from '../mol-math/linear-algebra';
-import { Renderable } from './renderable';
+import { GraphicsRenderable } from './renderable';
 import { Color } from '../mol-util/color';
 import { ValueCell, deepEqual } from '../mol-util';
-import { RenderableValues, GlobalUniformValues, BaseValues } from './renderable/schema';
+import { GlobalUniformValues } from './renderable/schema';
 import { GraphicsRenderVariant } from './webgl/render-item';
 import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { Clipping } from '../mol-theme/clipping';
 import { stringToWords } from '../mol-util/string';
 import { Transparency } from '../mol-theme/transparency';
 import { degToRad } from '../mol-math/misc';
-import { Texture } from './webgl/texture';
+import { createNullTexture, Texture, Textures } from './webgl/texture';
+import { arrayMapUpsert } from '../mol-util/array';
 
 export interface RendererStats {
     programCount: number
@@ -41,10 +42,21 @@ interface Renderer {
     readonly stats: RendererStats
     readonly props: Readonly<RendererProps>
 
-    clear: (transparentBackground: boolean) => void
-    render: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, drawingBufferScale: number, depthTexture: Texture | null) => void
+    clear: (toBackgroundColor: boolean) => void
+    clearDepth: () => void
+    update: (camera: ICamera) => void
+
+    renderPick: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, depthTexture: Texture | null) => void
+    renderDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderWboitOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderWboitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+
     setProps: (props: Partial<RendererProps>) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
+    setTransparentBackground: (value: boolean) => void
+    setDrawingBufferScale: (value: number) => void
+
     dispose: () => void
 }
 
@@ -167,6 +179,14 @@ namespace Renderer {
         const drawingBufferSize = Vec2.create(gl.drawingBufferWidth, gl.drawingBufferHeight);
         const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor);
 
+        let transparentBackground = false;
+        let drawingBufferScale = 1;
+
+        const nullDepthTexture = createNullTexture(gl, 'image-depth');
+        const sharedTexturesList: Textures = [
+            ['tDepth', nullDepthTexture]
+        ];
+
         const view = Mat4();
         const invView = Mat4();
         const modelView = Mat4();
@@ -204,6 +224,9 @@ namespace Renderer {
             uFogNear: ValueCell.create(1),
             uFogFar: ValueCell.create(10000),
             uFogColor: ValueCell.create(bgColor),
+
+            uRenderWboit: ValueCell.create(0),
+
             uTransparentBackground: ValueCell.create(false),
 
             uClipObjectType: ValueCell.create(clip.objects.type),
@@ -232,7 +255,7 @@ namespace Renderer {
 
         let globalUniformsNeedUpdate = true;
 
-        const renderObject = (r: Renderable<RenderableValues & BaseValues>, variant: GraphicsRenderVariant, depthTexture: Texture | null) => {
+        const renderObject = (r: GraphicsRenderable, variant: GraphicsRenderVariant) => {
             if (!r.state.visible || (!r.state.pickable && variant[0] === 'p')) {
                 return;
             }
@@ -265,26 +288,23 @@ namespace Renderer {
                 globalUniformsNeedUpdate = false;
             }
 
-            if (depthTexture) program.bindTextures([['tDepth', depthTexture]]);
-
             if (r.values.dRenderMode) { // indicates direct-volume
-                // always cull front
-                state.enable(gl.CULL_FACE);
-                state.frontFace(gl.CW);
-                state.cullFace(gl.BACK);
-
-                // depth test done manually in shader against `depthTexture`
-                // still need to enable when fragDepth can be used to write depth
-                // (unclear why depthMask is insufficient)
-                if (r.values.dRenderMode.ref.value === 'volume' || !fragDepth) {
-                    state.disable(gl.DEPTH_TEST);
-                    state.depthMask(false);
-                } else {
-                    state.enable(gl.DEPTH_TEST);
-                    state.depthMask(r.state.writeDepth);
+                // culling done in fragment shader
+                state.disable(gl.CULL_FACE);
+                state.frontFace(gl.CCW);
+
+                if (variant === 'colorBlended') {
+                    // depth test done manually in shader against `depthTexture`
+                    // still need to enable when fragDepth can be used to write depth
+                    if (r.values.dRenderMode.ref.value === 'volume' || !fragDepth) {
+                        state.disable(gl.DEPTH_TEST);
+                        state.depthMask(false);
+                    } else {
+                        state.enable(gl.DEPTH_TEST);
+                        state.depthMask(r.state.writeDepth);
+                    }
                 }
             } else {
-                state.enable(gl.DEPTH_TEST);
                 if (r.values.dDoubleSided) {
                     if (r.values.dDoubleSided.ref.value || r.values.hasReflection.ref.value) {
                         state.disable(gl.CULL_FACE);
@@ -310,22 +330,17 @@ namespace Renderer {
                     state.cullFace(gl.BACK);
                 }
 
-                state.depthMask(r.state.writeDepth);
+                if (variant === 'colorBlended') state.depthMask(r.state.writeDepth);
             }
 
-            r.render(variant);
+            r.render(variant, sharedTexturesList);
         };
 
-        const render = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, drawingBufferScale: number, depthTexture: Texture | null) => {
-            ValueCell.update(globalUniforms.uModel, group.view);
+        const update = (camera: ICamera) => {
             ValueCell.update(globalUniforms.uView, camera.view);
             ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view));
-            ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, group.view, camera.view));
-            ValueCell.update(globalUniforms.uInvModelView, Mat4.invert(invModelView, modelView));
             ValueCell.update(globalUniforms.uProjection, camera.projection);
             ValueCell.update(globalUniforms.uInvProjection, Mat4.invert(invProjection, camera.projection));
-            ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection));
-            ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection));
 
             ValueCell.updateIfChanged(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0);
             ValueCell.update(globalUniforms.uViewOffset, camera.viewOffset.enabled ? Vec2.set(viewOffset, camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2.set(viewOffset, 0, 0));
@@ -347,74 +362,150 @@ namespace Renderer {
                     gl.drawingBufferHeight * drawingBufferScale
                 ));
             }
+        };
 
-            globalUniformsNeedUpdate = true;
-            state.currentRenderItemId = -1;
+        const updateInternal = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, renderWboit: boolean) => {
+            arrayMapUpsert(sharedTexturesList, 'tDepth', depthTexture || nullDepthTexture);
 
-            const { renderables } = group;
+            ValueCell.update(globalUniforms.uModel, group.view);
+            ValueCell.update(globalUniforms.uModelView, Mat4.mul(modelView, group.view, camera.view));
+            ValueCell.update(globalUniforms.uInvModelView, Mat4.invert(invModelView, modelView));
+            ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection));
+            ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection));
+
+            ValueCell.updateIfChanged(globalUniforms.uRenderWboit, renderWboit ? 1 : 0);
 
             state.enable(gl.SCISSOR_TEST);
-            state.disable(gl.BLEND);
             state.colorMask(true, true, true, true);
-            state.enable(gl.DEPTH_TEST);
 
             const { x, y, width, height } = viewport;
             gl.viewport(x, y, width, height);
             gl.scissor(x, y, width, height);
 
-            if (clear) {
-                state.depthMask(true);
-                if (variant === 'color') {
-                    state.clearColor(bgColor[0], bgColor[1], bgColor[2], transparentBackground ? 0 : 1);
-                } else {
-                    state.clearColor(1, 1, 1, 1);
+            globalUniformsNeedUpdate = true;
+            state.currentRenderItemId = -1;
+        };
+
+        const renderPick = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, depthTexture: Texture | null) => {
+            state.disable(gl.BLEND);
+            state.enable(gl.DEPTH_TEST);
+            state.depthMask(true);
+
+            updateInternal(group, camera, depthTexture, false);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                if (!renderables[i].state.colorOnly) {
+                    renderObject(renderables[i], variant);
                 }
-                gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
             }
+        };
 
-            if (variant === 'color') {
-                for (let i = 0, il = renderables.length; i < il; ++i) {
-                    const r = renderables[i];
-                    if (r.state.opaque) {
-                        renderObject(r, variant, depthTexture);
-                    }
+        const renderDepth = (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);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                renderObject(renderables[i], 'depth');
+            }
+        };
+
+        const renderBlended = (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);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+                if (r.state.opaque) {
+                    renderObject(r, 'colorBlended');
                 }
+            }
 
-                state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
-                state.enable(gl.BLEND);
-                for (let i = 0, il = renderables.length; i < il; ++i) {
-                    const r = renderables[i];
-                    if (!r.state.opaque && r.state.writeDepth) {
-                        renderObject(r, variant, depthTexture);
-                    }
+            state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE);
+            state.enable(gl.BLEND);
+
+            state.depthMask(true);
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+                if (!r.state.opaque && r.state.writeDepth) {
+                    renderObject(r, 'colorBlended');
                 }
-                for (let i = 0, il = renderables.length; i < il; ++i) {
-                    const r = renderables[i];
-                    if (!r.state.opaque && !r.state.writeDepth) {
-                        renderObject(r, variant, depthTexture);
-                    }
+            }
+
+            state.depthMask(false);
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+                if (!r.state.opaque && !r.state.writeDepth) {
+                    renderObject(r, 'colorBlended');
                 }
-            } else { // picking & depth
-                for (let i = 0, il = renderables.length; i < il; ++i) {
-                    if (!renderables[i].state.colorOnly) {
-                        renderObject(renderables[i], variant, depthTexture);
-                    }
+            }
+        };
+
+        const renderWboitOpaque = (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);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+                if (r.values.uAlpha.ref.value === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values?.dRenderMode?.ref.value !== 'volume') {
+                    renderObject(r, 'colorWboit');
                 }
             }
+        };
+
+        const renderWboitTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            updateInternal(group, camera, depthTexture, true);
 
-            gl.flush();
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+                if (r.values.uAlpha.ref.value < 1 || r.values.transparencyAverage.ref.value > 0 || r.values?.dRenderMode?.ref.value === 'volume') {
+                    renderObject(r, 'colorWboit');
+                }
+            }
         };
 
         return {
-            clear: (transparentBackground: boolean) => {
-                ctx.unbindFramebuffer();
+            clear: (toBackgroundColor: boolean) => {
                 state.enable(gl.SCISSOR_TEST);
-                state.depthMask(true);
+                state.enable(gl.DEPTH_TEST);
                 state.colorMask(true, true, true, true);
-                state.clearColor(bgColor[0], bgColor[1], bgColor[2], transparentBackground ? 0 : 1);
+                state.depthMask(true);
+
+                if (transparentBackground) {
+                    state.clearColor(1, 1, 1, 0);
+                } else if (toBackgroundColor) {
+                    state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1);
+                } else {
+                    state.clearColor(1, 1, 1, 1);
+                }
                 gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
             },
-            render,
+            clearDepth: () => {
+                state.enable(gl.SCISSOR_TEST);
+                state.enable(gl.DEPTH_TEST);
+                state.depthMask(true);
+                gl.clear(gl.DEPTH_BUFFER_BIT);
+            },
+            update,
+
+            renderPick,
+            renderDepth,
+            renderBlended,
+            renderWboitOpaque,
+            renderWboitTransparent,
 
             setProps: (props: Partial<RendererProps>) => {
                 if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) {
@@ -481,6 +572,12 @@ namespace Renderer {
                     ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height));
                 }
             },
+            setTransparentBackground: (value: boolean) => {
+                transparentBackground = value;
+            },
+            setDrawingBufferScale: (value: number) => {
+                drawingBufferScale = value;
+            },
 
             get props() {
                 return p;

+ 14 - 15
src/mol-gl/scene.ts

@@ -5,9 +5,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Renderable } from './renderable';
 import { WebGLContext } from './webgl/context';
-import { RenderableValues, BaseValues } from './renderable/schema';
 import { GraphicsRenderObject, createRenderable } from './render-object';
 import { Object3D } from './object3d';
 import { Sphere3D } from '../mol-math/geometry';
@@ -16,10 +14,11 @@ import { now } from '../mol-util/now';
 import { arraySetRemove } from '../mol-util/array';
 import { BoundaryHelper } from '../mol-math/geometry/boundary-helper';
 import { hash1 } from '../mol-data/util';
+import { GraphicsRenderable } from './renderable';
 
 const boundaryHelper = new BoundaryHelper('98');
 
-function calculateBoundingSphere(renderables: Renderable<RenderableValues & BaseValues>[], boundingSphere: Sphere3D, onlyVisible: boolean): Sphere3D {
+function calculateBoundingSphere(renderables: GraphicsRenderable[], boundingSphere: Sphere3D, onlyVisible: boolean): Sphere3D {
     boundaryHelper.reset();
 
     for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -43,9 +42,9 @@ function calculateBoundingSphere(renderables: Renderable<RenderableValues & Base
     return boundaryHelper.getSphere(boundingSphere);
 }
 
-function renderableSort(a: Renderable<RenderableValues & BaseValues>, b: Renderable<RenderableValues & BaseValues>) {
-    const drawProgramIdA = a.getProgram('color').id;
-    const drawProgramIdB = b.getProgram('color').id;
+function renderableSort(a: GraphicsRenderable, b: GraphicsRenderable) {
+    const drawProgramIdA = a.getProgram('colorBlended').id;
+    const drawProgramIdB = b.getProgram('colorBlended').id;
     const materialIdA = a.materialId;
     const materialIdB = b.materialId;
 
@@ -62,7 +61,7 @@ function renderableSort(a: Renderable<RenderableValues & BaseValues>, b: Rendera
 
 interface Scene extends Object3D {
     readonly count: number
-    readonly renderables: ReadonlyArray<Renderable<RenderableValues & BaseValues>>
+    readonly renderables: ReadonlyArray<GraphicsRenderable>
     readonly boundingSphere: Sphere3D
     readonly boundingSphereVisible: Sphere3D
 
@@ -72,28 +71,28 @@ interface Scene extends Object3D {
     /** Returns `true` if some visibility has changed, `false` otherwise. */
     syncVisibility: () => boolean
     update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean) => void
-    add: (o: GraphicsRenderObject) => void // Renderable<any>
+    add: (o: GraphicsRenderObject) => void // GraphicsRenderable
     remove: (o: GraphicsRenderObject) => void
     commit: (maxTimeMs?: number) => boolean
     readonly needsCommit: boolean
     has: (o: GraphicsRenderObject) => boolean
     clear: () => void
-    forEach: (callbackFn: (value: Renderable<RenderableValues & BaseValues>, key: GraphicsRenderObject) => void) => void
+    forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void
 }
 
 namespace Scene {
     export interface Group extends Object3D {
-        readonly renderables: ReadonlyArray<Renderable<RenderableValues & BaseValues>>
+        readonly renderables: ReadonlyArray<GraphicsRenderable>
     }
 
     export function create(ctx: WebGLContext): Scene {
-        const renderableMap = new Map<GraphicsRenderObject, Renderable<RenderableValues & BaseValues>>();
-        const renderables: Renderable<RenderableValues & BaseValues>[] = [];
+        const renderableMap = new Map<GraphicsRenderObject, GraphicsRenderable>();
+        const renderables: GraphicsRenderable[] = [];
         const boundingSphere = Sphere3D();
         const boundingSphereVisible = Sphere3D();
 
-        const primitives: Renderable<RenderableValues & BaseValues>[] = [];
-        const volumes: Renderable<RenderableValues & BaseValues>[] = [];
+        const primitives: GraphicsRenderable[] = [];
+        const volumes: GraphicsRenderable[] = [];
 
         let boundingSphereDirty = true;
         let boundingSphereVisibleDirty = true;
@@ -223,7 +222,7 @@ namespace Scene {
                 boundingSphereDirty = true;
                 boundingSphereVisibleDirty = true;
             },
-            forEach: (callbackFn: (value: Renderable<any>, key: GraphicsRenderObject) => void) => {
+            forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => {
                 renderableMap.forEach(callbackFn);
             },
             get count() {

+ 25 - 11
src/mol-gl/shader-code.ts

@@ -59,6 +59,8 @@ 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 } = {
     apply_fog,
@@ -88,7 +90,9 @@ const ShaderChunks: { [k: string]: string } = {
     size_vert_params,
     texture3d_from_1d_trilinear,
     texture3d_from_2d_linear,
-    texture3d_from_2d_nearest
+    texture3d_from_2d_nearest,
+    wboit_params,
+    wboit_write
 };
 
 const reInclude = /^(?!\/\/)\s*#include\s+(\S+)/gmi;
@@ -115,31 +119,31 @@ export function ShaderCode(name: string, vert: string, frag: string, extensions:
 
 import points_vert from './shader/points.vert';
 import points_frag from './shader/points.frag';
-export const PointsShaderCode = ShaderCode('points', points_vert, points_frag);
+export const PointsShaderCode = ShaderCode('points', points_vert, points_frag, { drawBuffers: 'optional' });
 
 import spheres_vert from './shader/spheres.vert';
 import spheres_frag from './shader/spheres.frag';
-export const SpheresShaderCode = ShaderCode('spheres', spheres_vert, spheres_frag, { fragDepth: 'required' });
+export const SpheresShaderCode = ShaderCode('spheres', spheres_vert, spheres_frag, { fragDepth: 'required', drawBuffers: 'optional' });
 
 import text_vert from './shader/text.vert';
 import text_frag from './shader/text.frag';
-export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { standardDerivatives: 'required' });
+export const TextShaderCode = ShaderCode('text', text_vert, text_frag, { standardDerivatives: 'required', drawBuffers: 'optional' });
 
 import lines_vert from './shader/lines.vert';
 import lines_frag from './shader/lines.frag';
-export const LinesShaderCode = ShaderCode('lines', lines_vert, lines_frag);
+export const LinesShaderCode = ShaderCode('lines', lines_vert, lines_frag, { drawBuffers: 'optional' });
 
 import mesh_vert from './shader/mesh.vert';
 import mesh_frag from './shader/mesh.frag';
-export const MeshShaderCode = ShaderCode('mesh', mesh_vert, mesh_frag, { standardDerivatives: 'optional' });
+export const MeshShaderCode = ShaderCode('mesh', mesh_vert, mesh_frag, { standardDerivatives: 'optional', drawBuffers: 'optional' });
 
 import direct_volume_vert from './shader/direct-volume.vert';
 import direct_volume_frag from './shader/direct-volume.frag';
-export const DirectVolumeShaderCode = ShaderCode('direct-volume', direct_volume_vert, direct_volume_frag, { fragDepth: 'optional' });
+export const DirectVolumeShaderCode = ShaderCode('direct-volume', direct_volume_vert, direct_volume_frag, { fragDepth: 'optional', drawBuffers: 'optional' });
 
 import image_vert from './shader/image.vert';
 import image_frag from './shader/image.frag';
-export const ImageShaderCode = ShaderCode('image', image_vert, image_frag);
+export const ImageShaderCode = ShaderCode('image', image_vert, image_frag, { drawBuffers: 'optional' });
 
 //
 
@@ -186,6 +190,7 @@ function getGlsl100FragPrefix(extensions: WebGLExtensions, shaderExtensions: Sha
         if (extensions.drawBuffers) {
             prefix.push('#extension GL_EXT_draw_buffers : require');
             prefix.push('#define requiredDrawBuffers');
+            prefix.push('#define gl_FragColor gl_FragData[0]');
         } else if (shaderExtensions.drawBuffers === 'required') {
             throw new Error(`required 'GL_EXT_draw_buffers' extension not available`);
         }
@@ -198,6 +203,9 @@ function getGlsl100FragPrefix(extensions: WebGLExtensions, shaderExtensions: Sha
             throw new Error(`required 'GL_EXT_shader_texture_lod' extension not available`);
         }
     }
+    if (extensions.depthTexture) {
+        prefix.push('#define depthTextureSupport');
+    }
     return prefix.join('\n') + '\n';
 }
 
@@ -208,6 +216,8 @@ const glsl300VertPrefix = `#version 300 es
 `;
 
 const glsl300FragPrefixCommon = `
+layout(location = 0) out highp vec4 out_FragData0;
+
 #define varying in
 #define texture2D texture
 #define texture2DLodEXT textureLod
@@ -215,7 +225,7 @@ const glsl300FragPrefixCommon = `
 #define gl_FragColor out_FragData0
 #define gl_FragDepthEXT gl_FragDepth
 
-#define requiredDrawBuffers
+#define depthTextureSupport
 `;
 
 function getGlsl300FragPrefix(gl: WebGL2RenderingContext, extensions: WebGLExtensions, shaderExtensions: ShaderExtensions) {
@@ -226,12 +236,16 @@ function getGlsl300FragPrefix(gl: WebGL2RenderingContext, extensions: WebGLExten
     if (shaderExtensions.fragDepth) {
         prefix.push('#define enabledFragDepth');
     }
-    if (extensions.drawBuffers) {
+    if (shaderExtensions.drawBuffers) {
+        prefix.push('#define requiredDrawBuffers');
         const maxDrawBuffers = gl.getParameter(gl.MAX_DRAW_BUFFERS) as number;
-        for (let i = 0, il = maxDrawBuffers; i < il; ++i) {
+        for (let i = 1, il = maxDrawBuffers; i < il; ++i) {
             prefix.push(`layout(location = ${i}) out highp vec4 out_FragData${i};`);
         }
     }
+    if (shaderExtensions.shaderTextureLod) {
+        prefix.push('#define enabledShaderTextureLod');
+    }
     prefix.push(glsl300FragPrefixCommon);
     return prefix.join('\n') + '\n';
 }

+ 1 - 1
src/mol-gl/shader/chunks/apply-fog.glsl.ts

@@ -2,12 +2,12 @@ export default `
 float fogDepth = length(vViewPosition);
 float fogFactor = smoothstep(uFogNear, uFogFar, fogDepth);
 float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
+float preFogAlpha = gl_FragColor.a < 1.0 ? fogAlpha : 1.0;
 if (!uTransparentBackground) {
     gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
     if (gl_FragColor.a < 1.0)
         gl_FragColor.a = fogAlpha;
 } else {
-    float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
     gl_FragColor.a = fogAlpha;
 }
 `;

+ 21 - 16
src/mol-gl/shader/chunks/assign-material-color.glsl.ts

@@ -28,25 +28,30 @@ export default `
         if (ta < uPickingAlphaThreshold)
             discard; // ignore so the element below can be picked
     #else
-        float at = 0.0;
+        #if defined(dRenderVariant_colorBlended)
+            float at = 0.0;
 
-        // shift by view-offset during multi-sample rendering to allow for blending
-        vec2 coord = gl_FragCoord.xy + uViewOffset * 0.25;
+            // shift by view-offset during multi-sample rendering to allow for blending
+            vec2 coord = gl_FragCoord.xy + uViewOffset * 0.25;
 
-        #if defined(dTransparencyVariant_single)
-            const mat4 thresholdMatrix = mat4(
-                1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
-                13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
-                4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
-                16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
-            );
-            at = thresholdMatrix[int(intMod(coord.x, 4.0))][int(intMod(coord.y, 4.0))];
-        #elif defined(dTransparencyVariant_multi)
-            at = fract(dot(vec3(coord, vGroup + 0.5), vec3(2.0, 7.0, 23.0) / 17.0f));
-        #endif
+            #if defined(dTransparencyVariant_single)
+                const mat4 thresholdMatrix = mat4(
+                    1.0 / 17.0,  9.0 / 17.0,  3.0 / 17.0, 11.0 / 17.0,
+                    13.0 / 17.0,  5.0 / 17.0, 15.0 / 17.0,  7.0 / 17.0,
+                    4.0 / 17.0, 12.0 / 17.0,  2.0 / 17.0, 10.0 / 17.0,
+                    16.0 / 17.0,  8.0 / 17.0, 14.0 / 17.0,  6.0 / 17.0
+                );
+                at = thresholdMatrix[int(intMod(coord.x, 4.0))][int(intMod(coord.y, 4.0))];
+            #elif defined(dTransparencyVariant_multi)
+                at = fract(dot(vec3(coord, vGroup + 0.5), vec3(2.0, 7.0, 23.0) / 17.0f));
+            #endif
 
-        if (ta < 0.99 && (ta < 0.01 || ta < at))
-            discard;
+            if (ta < 0.99 && (ta < 0.01 || ta < at)) {
+                discard;
+            }
+        #elif defined(dRenderVariant_colorWboit)
+            material.a *= ta;
+        #endif
     #endif
 #endif
 `;

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

@@ -43,4 +43,6 @@ uniform float uInteriorDarkening;
 uniform bool uInteriorColorFlag;
 uniform vec3 uInteriorColor;
 bool interior;
+
+uniform mat4 uProjection;
 `;

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

@@ -1,6 +1,10 @@
 export default `
 // TODO find a better place for these convenience defines
 
+#if defined(dRenderVariant_colorBlended) || defined(dRenderVariant_colorWboit)
+    #define dRenderVariant_color
+#endif
+
 #if defined(dRenderVariant_pickObject) || defined(dRenderVariant_pickInstance) || defined(dRenderVariant_pickGroup)
     #define dRenderVariant_pick
 #endif

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

@@ -0,0 +1,20 @@
+export default `
+#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 int 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;
+}
+`;

+ 18 - 0
src/mol-gl/shader/chunks/wboit-write.glsl.ts

@@ -0,0 +1,18 @@
+export default `
+#if defined(dRenderVariant_colorWboit)
+    if (uRenderWboit == 0) {
+        if (preFogAlpha < 1.0) {
+            discard;
+        }
+    } else if (uRenderWboit == 1) {
+        if (preFogAlpha != 1.0 && !interior && fragmentDepth < getDepth(gl_FragCoord.xy / uDrawingBufferSize)) {
+            float alpha = preFogAlpha;
+            float wboitWeight = alpha * clamp(pow(1.0 - fragmentDepth, 2.0), 0.01, 1.0);
+            gl_FragColor = vec4(gl_FragColor.rgb * alpha * wboitWeight, alpha);
+            gl_FragData[1] = vec4(alpha * wboitWeight);
+        } else {
+            discard;
+        }
+    }
+#endif
+`;

+ 30 - 9
src/mol-gl/shader/direct-volume.frag.ts

@@ -121,21 +121,22 @@ uniform mat4 uCartnToUnit;
     }
 #endif
 
+#include wboit_params
+
 vec4 transferFunction(float value) {
     return texture2D(tTransferTex, vec2(value, 0.0));
 }
 
-// Calculate depth based on the given camera position.
-float calcDepth(const in vec3 cameraPos){
-    vec2 clipZW = cameraPos.z * uProjection[2].zw + uProjection[3].zw;
-    return 0.5 + 0.5 * clipZW.x / clipZW.y;
-}
-
 float getDepth(const in vec2 coords) {
-    #ifdef dPackedDepth
-        return unpackRGBAToDepth(texture2D(tDepth, coords));
+    #ifdef depthTextureSupport
+        if (uRenderWboit == 0) {
+            // in case of opaque volumes (and depth texture support)
+            return texture2D(tDepth, coords).r;
+        } else {
+            return unpackRGBAToDepth(texture2D(tDepth, coords));
+        }
     #else
-        return texture2D(tDepth, coords).r;
+        return unpackRGBAToDepth(texture2D(tDepth, coords));
     #endif
 }
 
@@ -145,6 +146,8 @@ vec3 v3m4(vec3 p, mat4 m) {
     return (m * vec4(p, 1.0)).xyz;
 }
 
+float preFogAlphaBlended = 0.0;
+
 vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
     #if defined(dRenderVariant_color) && !defined(dIgnoreLight)
         mat3 normalMatrix = transpose3(inverse3(mat3(uModelView * vTransform)));
@@ -321,6 +324,8 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
                     float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
                     #include apply_interior_color
                     #include apply_marker_color
+
+                    preFogAlphaBlended = (1.0 - preFogAlphaBlended) * gl_FragColor.a + preFogAlphaBlended;
                     #include apply_fog
 
                     src = gl_FragColor;
@@ -382,6 +387,8 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
 
                 float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
                 #include apply_marker_color
+
+                preFogAlphaBlended = (1.0 - preFogAlphaBlended) * gl_FragColor.a + preFogAlphaBlended;
                 #include apply_fog
 
                 src = gl_FragColor;
@@ -411,6 +418,9 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
 // TODO: support clipping exclusion texture support
 
 void main () {
+    if (gl_FrontFacing)
+        discard;
+
     #if defined(dRenderVariant_pick) || defined(dRenderVariant_depth)
         #if defined(dRenderMode_volume)
             // always ignore pick & depth for volume
@@ -433,5 +443,16 @@ void main () {
         if (gl_FragColor == vec4(0.0))
             discard;
     #endif
+
+    #if defined(dRenderVariant_color)
+        #if defined(dRenderMode_isosurface) && defined(enabledFragDepth)
+            float fragmentDepth = gl_FragDepthEXT;
+        #else
+            float fragmentDepth = calcDepth((uView * vec4(uCameraPosition + (d * rayDir), 1.0)).xyz);
+        #endif
+        float preFogAlpha = clamp(preFogAlphaBlended, 0.0, 1.0);
+        interior = false;
+        #include wboit_write
+    #endif
 }
 `;

+ 17 - 0
src/mol-gl/shader/evaluate-wboit.frag.ts

@@ -0,0 +1,17 @@
+export default `
+precision highp float;
+
+uniform sampler2D tWboitA;
+uniform sampler2D tWboitB;
+uniform vec2 uTexSize;
+
+void main() {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+
+    vec4 accum = texture2D(tWboitA, coords);
+    float r = 1.0 - accum.a;
+
+    accum.a = texture2D(tWboitB, coords).r;
+    gl_FragColor = vec4(accum.rgb / clamp(accum.a, 0.0001, 50000.0), r);
+}
+`;

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

@@ -10,6 +10,7 @@ precision highp int;
 #include common
 #include read_from_texture
 #include common_frag_params
+#include wboit_params
 
 uniform vec2 uImageTexDim;
 uniform sampler2D tImageTex;
@@ -122,6 +123,10 @@ void main() {
         float vMarker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
         #include apply_marker_color
         #include apply_fog
+
+        float fragmentDepth = gl_FragCoord.z;
+        bool interior = false;
+        #include wboit_write
     #endif
 }
 `;

+ 5 - 0
src/mol-gl/shader/lines.frag.ts

@@ -12,6 +12,7 @@ precision highp int;
 #include common_frag_params
 #include color_frag_params
 #include common_clip
+#include wboit_params
 
 void main(){
     #include clip_pixel
@@ -27,6 +28,10 @@ void main(){
 
         #include apply_marker_color
         #include apply_fog
+
+        float fragmentDepth = gl_FragCoord.z;
+        bool interior = false;
+        #include wboit_write
     #endif
 }
 `;

+ 4 - 0
src/mol-gl/shader/mesh.frag.ts

@@ -14,6 +14,7 @@ precision highp int;
 #include light_frag_params
 #include normal_frag_params
 #include common_clip
+#include wboit_params
 
 void main() {
     #include clip_pixel
@@ -59,6 +60,9 @@ void main() {
         #include apply_interior_color
         #include apply_marker_color
         #include apply_fog
+
+        float fragmentDepth = gl_FragCoord.z;
+        #include wboit_write
     #endif
 }
 `;

+ 5 - 0
src/mol-gl/shader/points.frag.ts

@@ -12,6 +12,7 @@ precision highp int;
 #include common_frag_params
 #include color_frag_params
 #include common_clip
+#include wboit_params
 
 #ifdef dPointFilledCircle
     uniform float uPointEdgeBleach;
@@ -41,6 +42,10 @@ void main(){
 
         #include apply_marker_color
         #include apply_fog
+
+        float fragmentDepth = gl_FragCoord.z;
+        bool interior = false;
+        #include wboit_write
     #endif
 }
 `;

+ 4 - 8
src/mol-gl/shader/spheres.frag.ts

@@ -13,8 +13,7 @@ precision highp int;
 #include color_frag_params
 #include light_frag_params
 #include common_clip
-
-uniform mat4 uProjection;
+#include wboit_params
 
 uniform float uClipNear;
 uniform float uIsOrtho;
@@ -27,12 +26,6 @@ varying vec3 vPointViewPosition;
 vec3 cameraPos;
 vec3 cameraNormal;
 
-// Calculate depth based on the given camera position.
-float calcDepth(const in vec3 cameraPos){
-    vec2 clipZW = cameraPos.z * uProjection[2].zw + uProjection[3].zw;
-    return 0.5 + 0.5 * clipZW.x / clipZW.y;
-}
-
 float calcClip(const in vec3 cameraPos) {
     return dot(vec4(cameraPos, 1.0), vec4(0.0, 0.0, 1.0, uClipNear - 0.5));
 }
@@ -114,6 +107,9 @@ void main(void){
         #include apply_interior_color
         #include apply_marker_color
         #include apply_fog
+
+        float fragmentDepth = gl_FragDepthEXT;
+        #include wboit_write
     #endif
 }
 `;

+ 5 - 0
src/mol-gl/shader/text.frag.ts

@@ -12,6 +12,7 @@ precision highp int;
 #include common_frag_params
 #include color_frag_params
 #include common_clip
+#include wboit_params
 
 uniform sampler2D tFont;
 
@@ -65,6 +66,10 @@ void main(){
     #elif defined(dRenderVariant_color)
         #include apply_marker_color
         #include apply_fog
+
+        float fragmentDepth = gl_FragCoord.z;
+        bool interior = false;
+        #include wboit_write
     #endif
 }
 `;

+ 9 - 4
src/mol-gl/webgl/context.ts

@@ -18,15 +18,17 @@ import { now } from '../../mol-util/now';
 import { Texture, TextureFilter } from './texture';
 import { ComputeRenderable } from '../renderable';
 
-export function getGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes): GLRenderingContext | null {
-    function getContext(contextId: 'webgl' | 'experimental-webgl' | 'webgl2') {
+export function getGLContext(canvas: HTMLCanvasElement, attribs?: WebGLContextAttributes): GLRenderingContext | null {
+    function get(id: 'webgl' | 'experimental-webgl' | 'webgl2') {
         try {
-            return canvas.getContext(contextId, contextAttributes) as GLRenderingContext | null;
+            return canvas.getContext(id, attribs) as GLRenderingContext | null;
         } catch (e) {
             return null;
         }
     }
-    return getContext('webgl2') ||  getContext('webgl') || getContext('experimental-webgl');
+    const gl = get('webgl2') || get('webgl') || get('experimental-webgl');
+    if (isDebugMode) console.log(`isWebgl2: ${isWebGL2(gl)}`);
+    return gl;
 }
 
 export function getErrorDescription(gl: GLRenderingContext, error: number) {
@@ -186,6 +188,7 @@ export interface WebGLContext {
     readonly maxTextureSize: number
     readonly maxRenderbufferSize: number
     readonly maxDrawBuffers: number
+    readonly maxTextureImageUnits: number
 
     readonly isContextLost: boolean
     readonly contextRestored: BehaviorSubject<now.Timestamp>
@@ -220,6 +223,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE) as number,
         maxRenderbufferSize: gl.getParameter(gl.MAX_RENDERBUFFER_SIZE) as number,
         maxDrawBuffers: isWebGL2(gl) ? gl.getParameter(gl.MAX_DRAW_BUFFERS) as number : 0,
+        maxTextureImageUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number,
         maxVertexTextureImageUnits: gl.getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS) as number,
     };
 
@@ -285,6 +289,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         get maxTextureSize () { return parameters.maxTextureSize; },
         get maxRenderbufferSize () { return parameters.maxRenderbufferSize; },
         get maxDrawBuffers () { return parameters.maxDrawBuffers; },
+        get maxTextureImageUnits () { return parameters.maxTextureImageUnits; },
 
         namedComputeRenderables: Object.create(null),
         namedFramebuffers: Object.create(null),

+ 0 - 1
src/mol-gl/webgl/extensions.ts

@@ -42,7 +42,6 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
     }
     const textureFloat = getTextureFloat(gl);
     if (isDebugMode && textureFloat === null) {
-        // TODO make sure non-support is handled downstream
         console.log('Could not find support for "texture_float"');
     }
     const textureFloatLinear = getTextureFloatLinear(gl);

+ 14 - 2
src/mol-gl/webgl/framebuffer.ts

@@ -50,15 +50,15 @@ function getFramebuffer(gl: GLRenderingContext) {
     return framebuffer;
 }
 
-export function createFramebuffer (gl: GLRenderingContext): Framebuffer {
+export function createFramebuffer(gl: GLRenderingContext): Framebuffer {
     let _framebuffer = getFramebuffer(gl);
 
     let destroyed = false;
 
     return {
         id: getNextFramebufferId(),
-        bind: () => gl.bindFramebuffer(gl.FRAMEBUFFER, _framebuffer),
 
+        bind: () => gl.bindFramebuffer(gl.FRAMEBUFFER, _framebuffer),
         reset: () => {
             _framebuffer = getFramebuffer(gl);
         },
@@ -68,4 +68,16 @@ export function createFramebuffer (gl: GLRenderingContext): Framebuffer {
             destroyed = true;
         }
     };
+}
+
+//
+
+export function createNullFramebuffer(): Framebuffer {
+    return {
+        id: getNextFramebufferId(),
+
+        bind: () => {},
+        reset: () => {},
+        destroy: () => {}
+    };
 }

+ 6 - 12
src/mol-gl/webgl/program.ts

@@ -24,7 +24,7 @@ export interface Program {
     use: () => void
     setUniforms: (uniformValues: UniformsList) => void
     bindAttributes: (attribueBuffers: AttributeBuffers) => void
-    bindTextures: (textures: Textures) => void
+    bindTextures: (textures: Textures, startingTargetUnit?: number) => void
 
     reset: () => void
     destroy: () => void
@@ -198,21 +198,15 @@ export function createProgram(gl: GLRenderingContext, state: WebGLState, extensi
                 if (l !== -1) buffer.bind(l);
             }
         },
-        bindTextures: (textures: Textures) => {
+        bindTextures: (textures: Textures, startingTargetUnit?: number) => {
+            startingTargetUnit = startingTargetUnit ?? 0;
+
             for (let i = 0, il = textures.length; i < il; ++i) {
                 const [k, texture] = textures[i];
                 const l = locations[k];
                 if (l !== null && l !== undefined) {
-                    if (k === 'tDepth') {
-                        // TODO find more explicit way?
-                        texture.bind(15 as TextureId);
-                        uniformSetters[k](gl, l, 15 as TextureId);
-                    } else {
-                        // TODO if the order and count of textures in a material can be made invariant
-                        //      bind needs to be called only when the material changes
-                        texture.bind(i as TextureId);
-                        uniformSetters[k](gl, l, i as TextureId);
-                    }
+                    texture.bind((i + startingTargetUnit) as TextureId);
+                    uniformSetters[k](gl, l, (i + startingTargetUnit) as TextureId);
                 }
             }
         },

+ 22 - 8
src/mol-gl/webgl/render-item.ts

@@ -5,7 +5,7 @@
  */
 
 import { createAttributeBuffers, ElementsBuffer, AttributeKind } from './buffer';
-import { createTextures, Texture } from './texture';
+import { createTextures, Texture, Textures } from './texture';
 import { WebGLContext, checkError } from './context';
 import { ShaderCode, DefineValues } from '../shader-code';
 import { Program } from './program';
@@ -40,14 +40,14 @@ export interface RenderItem<T extends string> {
     readonly materialId: number
     getProgram: (variant: T) => Program
 
-    render: (variant: T) => void
+    render: (variant: T, sharedTexturesList?: Textures) => void
     update: () => Readonly<ValueChanges>
     destroy: () => void
 }
 
 //
 
-const GraphicsRenderVariant = { 'color': '', 'pickObject': '', 'pickInstance': '', 'pickGroup': '', 'depth': '' };
+const GraphicsRenderVariant = { 'colorBlended': '', 'colorWboit': '', 'pickObject': '', 'pickInstance': '', 'pickGroup': '', 'depth': '' };
 export type GraphicsRenderVariant = keyof typeof GraphicsRenderVariant
 const GraphicsRenderVariants = Object.keys(GraphicsRenderVariant) as GraphicsRenderVariant[];
 
@@ -164,12 +164,17 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
         materialId,
         getProgram: (variant: T) => programs[variant],
 
-        render: (variant: T) => {
+        render: (variant: T, sharedTexturesList?: Textures) => {
             if (drawCount === 0 || instanceCount === 0 || ctx.isContextLost) return;
             const program = programs[variant];
             if (program.id === currentProgramId && state.currentRenderItemId === id) {
                 program.setUniforms(uniformValueEntries);
-                program.bindTextures(textures);
+                if (sharedTexturesList && sharedTexturesList.length > 0) {
+                    program.bindTextures(sharedTexturesList, 0);
+                    program.bindTextures(textures, sharedTexturesList.length);
+                } else {
+                    program.bindTextures(textures);
+                }
             } else {
                 const vertexArray = vertexArrays[variant];
                 if (program.id !== state.currentProgramId || program.id !== currentProgramId ||
@@ -182,7 +187,12 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                     currentProgramId = program.id;
                 }
                 program.setUniforms(uniformValueEntries);
-                program.bindTextures(textures);
+                if (sharedTexturesList && sharedTexturesList.length > 0) {
+                    program.bindTextures(sharedTexturesList, 0);
+                    program.bindTextures(textures, sharedTexturesList.length);
+                } else {
+                    program.bindTextures(textures);
+                }
                 if (vertexArray) {
                     vertexArray.bind();
                     // need to bind elements buffer explicitly since it is not always recorded in the VAO
@@ -194,7 +204,11 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                 state.currentRenderItemId = id;
             }
             if (isDebugMode) {
-                checkFramebufferStatus(ctx.gl);
+                try {
+                    checkFramebufferStatus(ctx.gl);
+                } catch (e) {
+                    throw new Error(`Framebuffer error rendering item id ${id}: '${e}'`);
+                }
             }
             if (elementsBuffer) {
                 instancedArrays.drawElementsInstanced(glDrawMode, drawCount, elementsBuffer._dataType, 0, instanceCount);
@@ -205,7 +219,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                 try {
                     checkError(ctx.gl);
                 } catch (e) {
-                    throw new Error(`Error rendering item id ${id}: '${e}'`);
+                    throw new Error(`Draw error rendering item id ${id}: '${e}'`);
                 }
             }
         },

+ 21 - 2
src/mol-gl/webgl/render-target.ts

@@ -5,8 +5,8 @@
  */
 
 import { idFactory } from '../../mol-util/id-factory';
-import { Texture, TextureFilter } from './texture';
-import { Framebuffer } from './framebuffer';
+import { createNullTexture, Texture, TextureFilter } from './texture';
+import { createNullFramebuffer, Framebuffer } from './framebuffer';
 import { WebGLResources } from './resources';
 import { GLRenderingContext } from './compat';
 
@@ -78,3 +78,22 @@ export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResou
         }
     };
 }
+
+//
+
+export function createNullRenderTarget(gl: GLRenderingContext): RenderTarget {
+    return {
+        id: getNextRenderTargetId(),
+        texture: createNullTexture(gl, 'image-uint8'),
+        framebuffer: createNullFramebuffer(),
+
+        getWidth: () => 0,
+        getHeight: () => 0,
+        bind: () => {
+            gl.bindFramebuffer(gl.FRAMEBUFFER, null);
+        },
+        setSize: () => {},
+        reset: () => {},
+        destroy: () => {}
+    };
+}

+ 33 - 0
src/mol-gl/webgl/texture.ts

@@ -312,4 +312,37 @@ export function createTextures(ctx: WebGLContext, schema: RenderableSchema, valu
         }
     });
     return textures;
+}
+
+//
+
+export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Texture {
+    const target = getTarget(gl, kind);
+    return {
+        id: getNextTextureId(),
+        target,
+        format: 0,
+        internalFormat: 0,
+        type: 0,
+
+        getWidth: () => 0,
+        getHeight: () => 0,
+        getDepth: () => 0,
+
+        define: () => {},
+        load: () => {},
+        bind: (id: TextureId) => {
+            gl.activeTexture(gl.TEXTURE0 + id);
+            gl.bindTexture(target, null);
+        },
+        unbind: (id: TextureId) => {
+            gl.activeTexture(gl.TEXTURE0 + id);
+            gl.bindTexture(target, null);
+        },
+        attachFramebuffer: () => {},
+        detachFramebuffer: () => {},
+
+        reset: () => {},
+        destroy: () => {},
+    };
 }

+ 4 - 4
src/mol-math/geometry/gaussian-density/gpu.ts

@@ -119,7 +119,7 @@ type GaussianDensityTextureData = {
 
 function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): GaussianDensityTextureData {
     // console.log('2d');
-    const { gl, resources, state, extensions } = webgl;
+    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat } } = webgl;
     const { smoothness, resolution } = props;
 
     const { drawCount, positions, radii, groups, scale, expandedBox, dim, maxRadius } = prepareGaussianDensityData(position, box, radius, props);
@@ -143,7 +143,7 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
     framebuffer.bind();
     setRenderingDefaults(webgl);
 
-    if (!texture) texture = extensions.colorBufferFloat
+    if (!texture) texture = colorBufferFloat && textureFloat
         ? resources.texture('image-float32', 'rgba', 'float', 'linear')
         : resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
     texture.define(texDimX, texDimY);
@@ -191,7 +191,7 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
 
 function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): GaussianDensityTextureData {
     // console.log('3d');
-    const { gl, resources, state, extensions } = webgl;
+    const { gl, resources, state, extensions: { colorBufferFloat, textureFloat } } = webgl;
     const { smoothness, resolution } = props;
 
     const { drawCount, positions, radii, groups, scale, expandedBox, dim, maxRadius } = prepareGaussianDensityData(position, box, radius, props);
@@ -214,7 +214,7 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
     setRenderingDefaults(webgl);
     gl.viewport(0, 0, dx, dy);
 
-    if (!texture) texture = extensions.colorBufferFloat
+    if (!texture) texture = colorBufferFloat && textureFloat
         ? resources.texture('volume-float32', 'rgba', 'float', 'linear')
         : resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear');
     texture.define(dx, dy, dz);

+ 2 - 1
src/mol-plugin/config.ts

@@ -24,7 +24,8 @@ export const PluginConfig = {
     General: {
         IsBusyTimeoutMs: item('plugin-config.is-busy-timeout', 750),
         DisableAntialiasing: item('plugin-config.disable-antialiasing', false),
-        PixelScale: item('plugin-config.pixel-scale', 1)
+        PixelScale: item('plugin-config.pixel-scale', 1),
+        EnableWboit: item('plugin-config.enable-wboit', false)
     },
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),

+ 2 - 1
src/mol-plugin/context.ts

@@ -190,7 +190,8 @@ export class PluginContext {
 
             const antialias = !(this.config.get(PluginConfig.General.DisableAntialiasing) ?? false);
             const pixelScale = this.config.get(PluginConfig.General.PixelScale) || 1;
-            (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas, {}, { antialias, pixelScale });
+            const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false;
+            (this.canvas3d as Canvas3D) = Canvas3D.fromCanvas(canvas, {}, { antialias, pixelScale, enableWboit });
             this.canvas3dInit.next(true);
             let props = this.spec.components?.viewport?.canvas3d;
 

+ 2 - 1
src/mol-plugin/util/viewport-screenshot.ts

@@ -107,12 +107,13 @@ class ViewportScreenshotHelper extends PluginComponent {
 
     private createPass(mutlisample: boolean) {
         const c = this.plugin.canvas3d!;
+        const { colorBufferFloat, textureFloat } = c.webgl.extensions;
         return this.plugin.canvas3d!.getImagePass({
             transparentBackground: this.values.transparent,
             cameraHelper: { axes: this.values.axes },
             multiSample: {
                 mode: mutlisample ? 'on' : 'off',
-                sampleLevel: c.webgl.extensions.colorBufferFloat ? 4 : 2
+                sampleLevel: colorBufferFloat && textureFloat ? 4 : 2
             },
             postprocessing: c.props.postprocessing
         });

+ 3 - 2
src/mol-repr/visual.ts

@@ -20,7 +20,7 @@ import { Overpaint } from '../mol-theme/overpaint';
 import { createOverpaint, clearOverpaint, applyOverpaintColor } from '../mol-geo/geometry/overpaint-data';
 import { Interval } from '../mol-data/int';
 import { Transparency } from '../mol-theme/transparency';
-import { createTransparency, clearTransparency, applyTransparencyValue } from '../mol-geo/geometry/transparency-data';
+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';
 
@@ -114,7 +114,7 @@ namespace Visual {
     export function setTransparency(renderObject: GraphicsRenderObject | undefined, transparency: Transparency, lociApply: LociApply, clear: boolean) {
         if (!renderObject) return;
 
-        const { tTransparency, uGroupCount, instanceCount } = renderObject.values;
+        const { tTransparency, transparencyAverage, uGroupCount, instanceCount } = renderObject.values;
         const count = uGroupCount.ref.value * instanceCount.ref.value;
 
         // ensure texture has right size and variant
@@ -134,6 +134,7 @@ namespace Visual {
             lociApply(loci, apply, false);
         }
         ValueCell.update(tTransparency, tTransparency.ref.value);
+        ValueCell.updateIfChanged(transparencyAverage, getTransparencyAverage(array, count));
     }
 
     export function setClipping(renderObject: GraphicsRenderObject | undefined, clipping: Clipping, lociApply: LociApply, clear: boolean) {

+ 11 - 1
src/mol-util/array.ts

@@ -122,4 +122,14 @@ export function arrayIsIdentity(xs: ArrayLike<number>) {
         if (xs[i] !== i) return false;
     }
     return true;
-}
+}
+
+export function arrayMapUpsert<T>(xs: [string, T][], key: string, value: T) {
+    for (let i = 0, il = xs.length; i < il; ++i) {
+        if (xs[i][0] === key) {
+            xs[i][1] = value;
+            return;
+        }
+    }
+    xs.push([key, value]);
+}