Explorar el Código

Merge pull request #106 from molstar/cylinders

Cylinders geomery and link visual improvements
David Sehnal hace 4 años
padre
commit
163929477e
Se han modificado 100 ficheros con 3428 adiciones y 789 borrados
  1. 1 0
      package.json
  2. 5 4
      src/apps/docking-viewer/viewport.tsx
  3. 2 2
      src/apps/viewer/index.html
  4. 1 1
      src/apps/viewer/index.ts
  5. 3 3
      src/examples/lighting/index.ts
  6. 7 4
      src/extensions/cellpack/color/provided.ts
  7. 10 1
      src/extensions/pdbe/structure-quality-report/color.ts
  8. 15 5
      src/extensions/rcsb/assembly-symmetry/color.ts
  9. 12 6
      src/extensions/rcsb/validation-report/color/density-fit.ts
  10. 27 23
      src/extensions/rcsb/validation-report/color/geometry-quality.ts
  11. 9 3
      src/extensions/rcsb/validation-report/color/random-coil-index.ts
  12. 3 3
      src/mol-canvas3d/camera.ts
  13. 9 7
      src/mol-canvas3d/canvas3d.ts
  14. 130 40
      src/mol-canvas3d/passes/draw.ts
  15. 130 0
      src/mol-canvas3d/passes/fxaa.ts
  16. 4 12
      src/mol-canvas3d/passes/image.ts
  17. 12 18
      src/mol-canvas3d/passes/multi-sample.ts
  18. 1 5
      src/mol-canvas3d/passes/passes.ts
  19. 407 163
      src/mol-canvas3d/passes/postprocessing.ts
  20. 249 0
      src/mol-canvas3d/passes/smaa.ts
  21. 14 6
      src/mol-canvas3d/passes/wboit.ts
  22. 102 0
      src/mol-geo/geometry/cylinders/cylinders-builder.ts
  23. 278 0
      src/mol-geo/geometry/cylinders/cylinders.ts
  24. 18 11
      src/mol-geo/geometry/geometry.ts
  25. 2 2
      src/mol-geo/geometry/lines/lines.ts
  26. 1 1
      src/mol-geo/geometry/points/points.ts
  27. 9 6
      src/mol-gl/render-object.ts
  28. 40 0
      src/mol-gl/renderable/cylinders.ts
  29. 1 0
      src/mol-gl/renderable/schema.ts
  30. 45 15
      src/mol-gl/renderer.ts
  31. 6 0
      src/mol-gl/shader-code.ts
  32. 2 2
      src/mol-gl/shader/chunks/apply-fog.glsl.ts
  33. 2 2
      src/mol-gl/shader/chunks/check-picking-alpha.glsl.ts
  34. 1 1
      src/mol-gl/shader/chunks/color-frag-params.glsl.ts
  35. 2 2
      src/mol-gl/shader/chunks/color-vert-params.glsl.ts
  36. 4 2
      src/mol-gl/shader/chunks/common-clip.glsl.ts
  37. 7 2
      src/mol-gl/shader/chunks/common-frag-params.glsl.ts
  38. 3 2
      src/mol-gl/shader/chunks/common-vert-params.glsl.ts
  39. 34 3
      src/mol-gl/shader/chunks/common.glsl.ts
  40. 50 50
      src/mol-gl/shader/chunks/light-frag-params.glsl.ts
  41. 7 0
      src/mol-gl/shader/chunks/wboit-write.glsl.ts
  42. 12 0
      src/mol-gl/shader/copy.frag.ts
  43. 139 0
      src/mol-gl/shader/cylinders.frag.ts
  44. 74 0
      src/mol-gl/shader/cylinders.vert.ts
  45. 8 4
      src/mol-gl/shader/direct-volume.frag.ts
  46. 7 3
      src/mol-gl/shader/image.frag.ts
  47. 3 3
      src/mol-gl/shader/lines.frag.ts
  48. 0 1
      src/mol-gl/shader/lines.vert.ts
  49. 1 2
      src/mol-gl/shader/mesh.frag.ts
  50. 65 0
      src/mol-gl/shader/outlines.frag.ts
  51. 3 3
      src/mol-gl/shader/points.frag.ts
  52. 80 67
      src/mol-gl/shader/postprocessing.frag.ts
  53. 66 0
      src/mol-gl/shader/smaa/blend.frag.ts
  54. 35 0
      src/mol-gl/shader/smaa/blend.vert.ts
  55. 76 0
      src/mol-gl/shader/smaa/edges.frag.ts
  56. 36 0
      src/mol-gl/shader/smaa/edges.vert.ts
  57. 216 0
      src/mol-gl/shader/smaa/weights.frag.ts
  58. 42 0
      src/mol-gl/shader/smaa/weights.vert.ts
  59. 5 17
      src/mol-gl/shader/spheres.frag.ts
  60. 84 0
      src/mol-gl/shader/ssao-blur.frag.ts
  61. 110 0
      src/mol-gl/shader/ssao.frag.ts
  62. 3 3
      src/mol-gl/shader/text.frag.ts
  63. 1 1
      src/mol-gl/webgl/render-target.ts
  64. 25 4
      src/mol-gl/webgl/texture.ts
  65. 10 35
      src/mol-math/geometry/gaussian-density.ts
  66. 8 8
      src/mol-math/geometry/gaussian-density/gpu.ts
  67. 5 0
      src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
  68. 5 0
      src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
  69. 11 3
      src/mol-model-props/computed/themes/accessible-surface-area.ts
  70. 8 4
      src/mol-model/custom-property.ts
  71. 1 3
      src/mol-plugin-state/transforms/representation.ts
  72. 15 0
      src/mol-plugin-state/transforms/volume.ts
  73. 1 1
      src/mol-plugin/config.ts
  74. 11 8
      src/mol-repr/structure/complex-representation.ts
  75. 27 5
      src/mol-repr/structure/complex-visual.ts
  76. 4 1
      src/mol-repr/structure/params.ts
  77. 4 4
      src/mol-repr/structure/representation/ball-and-stick.ts
  78. 2 2
      src/mol-repr/structure/representation/ellipsoid.ts
  79. 3 6
      src/mol-repr/structure/representation/gaussian-surface.ts
  80. 1 1
      src/mol-repr/structure/representation/line.ts
  81. 3 3
      src/mol-repr/structure/representation/spacefill.ts
  82. 47 31
      src/mol-repr/structure/units-representation.ts
  83. 25 4
      src/mol-repr/structure/units-visual.ts
  84. 84 18
      src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts
  85. 1 0
      src/mol-repr/structure/visual/bond-inter-unit-line.ts
  86. 93 18
      src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts
  87. 1 0
      src/mol-repr/structure/visual/bond-intra-unit-line.ts
  88. 12 3
      src/mol-repr/structure/visual/element-sphere.ts
  89. 12 6
      src/mol-repr/structure/visual/gaussian-density-volume.ts
  90. 55 14
      src/mol-repr/structure/visual/gaussian-surface-mesh.ts
  91. 2 3
      src/mol-repr/structure/visual/gaussian-surface-wireframe.ts
  92. 15 7
      src/mol-repr/structure/visual/util/bond.ts
  93. 4 4
      src/mol-repr/structure/visual/util/common.ts
  94. 17 23
      src/mol-repr/structure/visual/util/gaussian.ts
  95. 93 18
      src/mol-repr/structure/visual/util/link.ts
  96. 1 0
      src/mol-repr/visual.ts
  97. 4 3
      src/mol-repr/volume/direct-volume.ts
  98. 69 32
      src/mol-repr/volume/isosurface.ts
  99. 16 6
      src/mol-repr/volume/representation.ts
  100. 2 0
      src/mol-state/object.ts

+ 1 - 0
package.json

@@ -80,6 +80,7 @@
     "Alexander Rose <alexander.rose@weirdbyte.de>",
     "David Sehnal <david.sehnal@gmail.com>",
     "Sebastian Bittrich <sebastian.bittrich@rcsb.org>",
+    "Áron Samuel Kovács <aron.kovacs@mail.muni.cz>",
     "Ludovic Autin <autin@scripps.edu>",
     "Michal Malý <michal.maly@ibt.cas.cz>",
     "Jiří Černý <jiri.cerny@ibt.cas.cz>"

+ 5 - 4
src/apps/docking-viewer/viewport.tsx

@@ -46,13 +46,14 @@ function occlusionStyle(plugin: PluginContext) {
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
             occlusion: { name: 'on', params: {
-                kernelSize: 8,
-                bias: 0.8,
-                radius: 64
+                samples: 64,
+                radius: 8,
+                bias: 1.0,
+                blurKernelSize: 13
             } },
             outline: { name: 'on', params: {
                 scale: 1.0,
-                threshold: 0.8
+                threshold: 0.33
             } }
         }
     } });

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

@@ -50,14 +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 disableWboit = getParam('disable-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,
+                enableWboit: !disableWboit,
                 layoutShowControls: !hideControls,
                 viewportShowExpand: false,
                 pdbProvider: pdbProvider || 'pdbe',

+ 1 - 1
src/apps/viewer/index.ts

@@ -69,7 +69,7 @@ const DefaultViewerOptions = {
     layoutShowLeftPanel: true,
     disableAntialiasing: false,
     pixelScale: 1,
-    enableWboit: false,
+    enableWboit: true,
 
     viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,

+ 3 - 3
src/examples/lighting/index.ts

@@ -24,8 +24,8 @@ const Canvas3DPresets = {
             mode: 'temporal' as Canvas3DProps['multiSample']['mode']
         },
         postprocessing: {
-            occlusion: { name: 'on', params: { bias: 0.8, kernelSize: 6, radius: 64 } },
-            outline: { name: 'on', params: { scale: 1, threshold: 0.8 } }
+            occlusion: { name: 'on', params: { samples: 64, radius: 8, bias: 1.0, blurKernelSize: 13 } },
+            outline: { name: 'on', params: { scale: 1, threshold: 0.33 } }
         },
         renderer: {
             ambientIntensity: 1,
@@ -37,7 +37,7 @@ const Canvas3DPresets = {
             mode: 'temporal' as Canvas3DProps['multiSample']['mode']
         },
         postprocessing: {
-            occlusion: { name: 'on', params: { bias: 0.8, kernelSize: 6, radius: 64 } },
+            occlusion: { name: 'on', params: { samples: 64, radius: 8, bias: 1.0, blurKernelSize: 13 } },
             outline: { name: 'off', params: { } }
         },
         renderer: {

+ 7 - 4
src/extensions/cellpack/color/provided.ts

@@ -9,7 +9,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Color } from '../../../mol-util/color';
 import { ColorTheme, LocationColor } from '../../../mol-theme/color';
 import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
-import { StructureElement, Model } from '../../../mol-model/structure';
+import { StructureElement, Model, Bond } from '../../../mol-model/structure';
 import { Location } from '../../../mol-model/location';
 import { CellPackInfoProvider } from '../property';
 
@@ -37,9 +37,12 @@ export function CellPackProvidedColorTheme(ctx: ThemeDataContext, props: PD.Valu
         }
 
         color = (location: Location): Color => {
-            return StructureElement.Location.is(location)
-                ? modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!
-                : DefaultColor;
+            if (StructureElement.Location.is(location)) {
+                return modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!;
+            } else if (Bond.isLocation(location)) {
+                return modelColor.get(Model.TrajectoryInfo.get(location.aUnit.model).index)!;
+            }
+            return DefaultColor;
         };
     } else {
         color = () => DefaultColor;

+ 10 - 1
src/extensions/pdbe/structure-quality-report/color.ts

@@ -6,7 +6,7 @@
 
 import { StructureQualityReport, StructureQualityReportProvider } from './prop';
 import { Location } from '../../../mol-model/location';
-import { StructureElement } from '../../../mol-model/structure';
+import { Bond, StructureElement } from '../../../mol-model/structure';
 import { ColorTheme, LocationColor } from '../../../mol-theme/color';
 import { ThemeDataContext } from '../../../mol-theme/theme';
 import { Color } from '../../../mol-util/color';
@@ -46,11 +46,16 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
 
     if (ctx.structure && !ctx.structure.isEmpty && ctx.structure.models[0].customProperties.has(StructureQualityReportProvider.descriptor)) {
         const getIssues = StructureQualityReport.getIssues;
+        const l = StructureElement.Location.create(ctx.structure);
 
         if (props.type.name === 'issue-count') {
             color = (location: Location) => {
                 if (StructureElement.Location.is(location)) {
                     return ValidationColors[Math.min(3, getIssues(location).length) + 1];
+                } else if (Bond.isLocation(location)) {
+                    l.unit = location.aUnit;
+                    l.element = location.aUnit.elements[location.aIndex];
+                    return ValidationColors[Math.min(3, getIssues(l).length) + 1];
                 }
                 return ValidationColors[0];
             };
@@ -59,6 +64,10 @@ export function StructureQualityReportColorTheme(ctx: ThemeDataContext, props: P
             color = (location: Location) => {
                 if (StructureElement.Location.is(location) && getIssues(location).indexOf(issue) >= 0) {
                     return ValidationColors[4];
+                } else if (Bond.isLocation(location)) {
+                    l.unit = location.aUnit;
+                    l.element = location.aUnit.elements[location.aIndex];
+                    return ValidationColors[Math.min(3, getIssues(l).length) + 1];
                 }
                 return ValidationColors[0];
             };

+ 15 - 5
src/extensions/rcsb/assembly-symmetry/color.ts

@@ -9,7 +9,7 @@ import { ColorTheme, LocationColor } from '../../../mol-theme/color';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { AssemblySymmetryProvider, AssemblySymmetry } from './prop';
 import { Color } from '../../../mol-util/color';
-import { Unit, StructureElement, StructureProperties } from '../../../mol-model/structure';
+import { Unit, StructureElement, StructureProperties, Bond } from '../../../mol-model/structure';
 import { Location } from '../../../mol-model/location';
 import { ScaleLegend, TableLegend } from '../../../mol-util/legend';
 import { getPalette, getPaletteParams } from '../../../mol-util/color/palette';
@@ -50,6 +50,8 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
     const clusters = assemblySymmetry?.value?.clusters;
 
     if (clusters?.length && ctx.structure) {
+        const l = StructureElement.Location.create(ctx.structure);
+
         const clusterByMember = new Map<string, number>();
         for (let i = 0, il = clusters.length; i < il; ++i) {
             const { members } = clusters[i]!;
@@ -67,12 +69,20 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
         legend = palette.legend;
 
         const _emptyList: any[] = [];
+        const getColor = (location: StructureElement.Location) => {
+            const { assembly } = location.unit.conformation.operator;
+            const asymId = getAsymId(location.unit)(location);
+            const cluster = clusterByMember.get(clusterMemberKey(asymId, assembly?.operList || _emptyList));
+            return cluster !== undefined ? palette.color(cluster) : DefaultColor;
+        };
+
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location)) {
-                const { assembly } = location.unit.conformation.operator;
-                const asymId = getAsymId(location.unit)(location);
-                const cluster = clusterByMember.get(clusterMemberKey(asymId, assembly?.operList || _emptyList));
-                return cluster !== undefined ? palette.color(cluster) : DefaultColor;
+                return getColor(location);
+            } else if (Bond.isLocation(location)) {
+                l.unit = location.aUnit;
+                l.element = location.aUnit.elements[location.aIndex];
+                return getColor(l);
             }
             return DefaultColor;
         };

+ 12 - 6
src/extensions/rcsb/validation-report/color/density-fit.ts

@@ -8,7 +8,7 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
 import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { Color, ColorScale } from '../../../../mol-util/color';
-import { StructureElement, Model } from '../../../../mol-model/structure';
+import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-model/structure';
 import { Location } from '../../../../mol-model/location';
 import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
 import { ValidationReportProvider, ValidationReport } from '../prop';
@@ -37,13 +37,19 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
     if (validationReport?.value && model) {
         const { rsrz, rscc } = validationReport.value;
         const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
+        const getColor = (element: ElementIndex) => {
+            const rsrzValue = rsrz.get(residueIndex[element]);
+            if (rsrzValue !== undefined) return scaleRsrz.color(rsrzValue);
+            const rsccValue = rscc.get(residueIndex[element]);
+            if (rsccValue !== undefined) return scaleRscc.color(rsccValue);
+            return DefaultColor;
+        };
+
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location) && location.unit.model === model) {
-                const rsrzValue = rsrz.get(residueIndex[location.element]);
-                if (rsrzValue !== undefined) return scaleRsrz.color(rsrzValue);
-                const rsccValue = rscc.get(residueIndex[location.element]);
-                if (rsccValue !== undefined) return scaleRscc.color(rsccValue);
-                return DefaultColor;
+                return getColor(location.element);
+            } else if (Bond.isLocation(location) && location.aUnit.model === model) {
+                return getColor(location.aUnit.elements[location.aIndex]);
             }
             return DefaultColor;
         };

+ 27 - 23
src/extensions/rcsb/validation-report/color/geometry-quality.ts

@@ -8,7 +8,7 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
 import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { Color } from '../../../../mol-util/color';
-import { StructureElement } from '../../../../mol-model/structure';
+import { Bond, ElementIndex, StructureElement } from '../../../../mol-model/structure';
 import { Location } from '../../../../mol-model/location';
 import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
 import { ValidationReportProvider, ValidationReport } from '../prop';
@@ -59,31 +59,35 @@ export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Value
         const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
         const { polymerType } = model.atomicHierarchy.derived.residue;
         const ignore = new Set(props.ignore);
+        const getColor = (element: ElementIndex) => {
+            const rI = residueIndex[element];
+
+            const value = geometryIssues.get(rI);
+            if (value === undefined) return DefaultColor;
+
+            let count = SetUtils.differenceSize(value, ignore);
+
+            if (count > 0 && polymerType[rI] === PolymerType.NA) {
+                count = 0;
+                if (!ignore.has('clash') && clashes.getVertexEdgeCount(element) > 0) count += 1;
+                if (!ignore.has('mog-bond-outlier') && bondOutliers.index.has(element)) count += 1;
+                if (!ignore.has('mog-angle-outlier') && angleOutliers.index.has(element)) count += 1;
+            }
+
+            switch (count) {
+                case undefined: return DefaultColor;
+                case 0: return NoIssuesColor;
+                case 1: return OneIssueColor;
+                case 2: return TwoIssuesColor;
+                default: return ThreeOrMoreIssuesColor;
+            }
+        };
 
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location) && location.unit.model === model) {
-                const { element } = location;
-                const rI = residueIndex[element];
-
-                const value = geometryIssues.get(rI);
-                if (value === undefined) return DefaultColor;
-
-                let count = SetUtils.differenceSize(value, ignore);
-
-                if (count > 0 && polymerType[rI] === PolymerType.NA) {
-                    count = 0;
-                    if (!ignore.has('clash') && clashes.getVertexEdgeCount(element) > 0) count += 1;
-                    if (!ignore.has('mog-bond-outlier') && bondOutliers.index.has(element)) count += 1;
-                    if (!ignore.has('mog-angle-outlier') && angleOutliers.index.has(element)) count += 1;
-                }
-
-                switch (count) {
-                    case undefined: return DefaultColor;
-                    case 0: return NoIssuesColor;
-                    case 1: return OneIssueColor;
-                    case 2: return TwoIssuesColor;
-                    default: return ThreeOrMoreIssuesColor;
-                }
+                return getColor(location.element);
+            } else if (Bond.isLocation(location) && location.aUnit.model === model) {
+                return getColor(location.aUnit.elements[location.aIndex]);
             }
             return DefaultColor;
         };

+ 9 - 3
src/extensions/rcsb/validation-report/color/random-coil-index.ts

@@ -8,7 +8,7 @@ import { ThemeDataContext } from '../../../../mol-theme/theme';
 import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { Color, ColorScale } from '../../../../mol-util/color';
-import { StructureElement, Model } from '../../../../mol-model/structure';
+import { StructureElement, Model, ElementIndex, Bond } from '../../../../mol-model/structure';
 import { Location } from '../../../../mol-model/location';
 import { CustomProperty } from '../../../../mol-model-props/common/custom-property';
 import { ValidationReportProvider, ValidationReport } from '../prop';
@@ -31,10 +31,16 @@ export function RandomCoilIndexColorTheme(ctx: ThemeDataContext, props: {}): Col
 
     if (rci && model) {
         const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
+        const getColor = (element: ElementIndex) => {
+            const value = rci.get(residueIndex[element]);
+            return value === undefined ? DefaultColor : scale.color(value);
+        };
+
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location) && location.unit.model === model) {
-                const value = rci.get(residueIndex[location.element]);
-                return value === undefined ? DefaultColor : scale.color(value);
+                return getColor(location.element);
+            } else if (Bond.isLocation(location) && location.aUnit.model === model) {
+                return getColor(location.aUnit.elements[location.aIndex]);
             }
             return DefaultColor;
         };

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

@@ -319,8 +319,8 @@ function updateClip(camera: Camera) {
     let far = cameraDistance + normalizedFar;
 
     const fogNearFactor = -(50 - fog) / 50;
-    let fogNear = cameraDistance - (normalizedFar * fogNearFactor);
-    let fogFar = far;
+    const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
+    const fogFar = far;
 
     if (mode === 'perspective') {
         // set at least to 5 to avoid slow sphere impostor rendering
@@ -337,7 +337,7 @@ function updateClip(camera: Camera) {
     }
 
     camera.near = near;
-    camera.far = far;
+    camera.far = 2 * far; // avoid precision issues distingushing far objects from background
     camera.fogNear = fogNear;
     camera.fogFar = fogFar;
 }

+ 9 - 7
src/mol-canvas3d/canvas3d.ts

@@ -24,7 +24,7 @@ import { ParamDefinition as PD } from '../mol-util/param-definition';
 import { DebugHelperParams } from './helper/bounding-sphere-helper';
 import { SetUtils } from '../mol-util/set';
 import { Canvas3dInteractionHelper } from './helper/interaction-events';
-import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
+import { PostprocessingParams } from './passes/postprocessing';
 import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
 import { PickData } from './passes/pick';
 import { PickHelper } from './passes/pick';
@@ -51,7 +51,7 @@ export const Canvas3DParams = {
     }, { pivot: 'mode' }),
     cameraFog: PD.MappedStatic('on', {
         on: PD.Group({
-            intensity: PD.Numeric(50, { min: 1, max: 100, step: 1 }),
+            intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Show fog in the distance' }),
@@ -341,9 +341,11 @@ namespace Canvas3D {
         function render(force: boolean) {
             if (webgl.isContextLost) return false;
 
+            let resized = false;
             if (resizeRequested) {
                 handleResize(false);
                 resizeRequested = false;
+                resized = true;
             }
 
             if (x > gl.drawingBufferWidth || x + width < 0 ||
@@ -355,7 +357,7 @@ namespace Canvas3D {
             const cameraChanged = camera.update();
             const multiSampleChanged = multiSampleHelper.update(force || cameraChanged, p.multiSample);
 
-            if (force || cameraChanged || multiSampleChanged) {
+            if (resized || force || cameraChanged || multiSampleChanged) {
                 let cam: Camera | StereoCamera = camera;
                 if (p.camera.stereo.name === 'on') {
                     stereoCamera.update();
@@ -365,9 +367,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 && !passes.draw.wboitEnabled;
-                    passes.draw.render(renderer, cam, scene, helper, toDrawingBuffer, p.transparentBackground);
-                    if (!toDrawingBuffer) passes.postprocessing.render(cam, true, p.postprocessing);
+                    passes.draw.render(renderer, cam, scene, helper, true, p.transparentBackground, p.postprocessing);
                 }
                 pickHelper.dirty = true;
                 didRender = true;
@@ -692,10 +692,12 @@ namespace Canvas3D {
             reprCount,
             resized,
             setProps: (properties, doNotRequestDraw = false) => {
-                const props: PartialCanvas3DProps = typeof properties === 'function'
+                let props: PartialCanvas3DProps = typeof properties === 'function'
                     ? produce(getProps(), properties)
                     : properties;
 
+                props = PD.normalizeParams(Canvas3DParams, props, 'children');
+
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null);
                 if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
                     cameraState.mode = props.camera.mode;

+ 130 - 40
src/mol-canvas3d/passes/draw.ts

@@ -22,8 +22,10 @@ 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 { copy_frag } from '../../mol-gl/shader/copy.frag';
 import { StereoCamera } from '../camera/stereo';
 import { WboitPass } from './wboit';
+import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
 
 const DepthMergeSchema = {
     ...QuadSchema,
@@ -50,6 +52,27 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text
     return createComputeRenderable(renderItem, values);
 }
 
+const CopySchema = {
+    ...QuadSchema,
+    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+};
+const  CopyShaderCode = ShaderCode('copy', quad_vert, copy_frag);
+type  CopyRenderable = ComputeRenderable<Values<typeof CopySchema>>
+
+function getCopyRenderable(ctx: WebGLContext, colorTexture: Texture): CopyRenderable {
+    const values: Values<typeof CopySchema> = {
+        ...QuadValues,
+        tColor: ValueCell.create(colorTexture),
+        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
+    };
+
+    const schema = { ...CopySchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', CopyShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
 export class DrawPass {
     private readonly drawTarget: RenderTarget
 
@@ -57,17 +80,23 @@ export class DrawPass {
     readonly depthTexture: Texture
     readonly depthTexturePrimitives: Texture
 
-    private readonly packedDepth: boolean
+    readonly packedDepth: boolean
+
     private depthTarget: RenderTarget
     private depthTargetPrimitives: RenderTarget | null
     private depthTargetVolumes: RenderTarget | null
     private depthTextureVolumes: Texture
     private depthMerge: DepthMergeRenderable
 
+    private copyFboTarget: CopyRenderable
+    private copyFboPostprocessing: CopyRenderable
+
     private wboit: WboitPass | undefined
+    readonly postprocessing: PostprocessingPass
+    private readonly antialiasing: AntialiasingPass
 
     get wboitEnabled() {
-        return !!this.wboit?.enabled;
+        return !!this.wboit?.supported;
     }
 
     constructor(private webgl: WebGLContext, width: number, height: number, enableWboit: boolean) {
@@ -93,6 +122,11 @@ export class DrawPass {
         this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
 
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
+        this.postprocessing = new PostprocessingPass(webgl, this);
+        this.antialiasing = new AntialiasingPass(webgl, this);
+
+        this.copyFboTarget = getCopyRenderable(webgl, this.colorTarget.texture);
+        this.copyFboPostprocessing = getCopyRenderable(webgl, this.postprocessing.target.texture);
     }
 
     reset() {
@@ -121,9 +155,15 @@ export class DrawPass {
 
             ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height));
 
-            if (this.wboit?.enabled) {
+            ValueCell.update(this.copyFboTarget.values.uTexSize, Vec2.set(this.copyFboTarget.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.copyFboPostprocessing.values.uTexSize, Vec2.set(this.copyFboPostprocessing.values.uTexSize.ref.value, width, height));
+
+            if (this.wboit?.supported) {
                 this.wboit.setSize(width, height);
             }
+
+            this.postprocessing.setSize(width, height);
+            this.antialiasing.setSize(width, height);
         }
     }
 
@@ -141,41 +181,50 @@ export class DrawPass {
         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');
+    private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
+        if (!this.wboit?.supported) throw new Error('expected wboit to be supported');
 
-        const renderTarget = toDrawingBuffer ? this.drawTarget : this.colorTarget;
-        renderTarget.bind();
+        this.colorTarget.bind();
         renderer.clear(true);
 
         // render opaque primitives
-        this.depthTexturePrimitives.attachFramebuffer(renderTarget.framebuffer, 'depth');
-        renderTarget.bind();
+        this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+        this.colorTarget.bind();
+        renderer.clearDepth();
         renderer.renderWboitOpaque(scene.primitives, camera, null);
 
         // render opaque volumes
-        this.depthTextureVolumes.attachFramebuffer(renderTarget.framebuffer, 'depth');
-        renderTarget.bind();
+        this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+        this.colorTarget.bind();
         renderer.clearDepth();
         renderer.renderWboitOpaque(scene.volumes, camera, this.depthTexturePrimitives);
 
         // merge depth of opaque primitives and volumes
         this._depthMerge();
 
+        if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
+        }
+
         // 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();
+        if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            this.depthTexturePrimitives.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
+            this.postprocessing.target.bind();
+        } else {
+            this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+            this.colorTarget.bind();
+        }
         this.wboit.render();
     }
 
-    private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean) {
+    private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
         if (toDrawingBuffer) {
-            this.webgl.unbindFramebuffer();
+            this.drawTarget.bind();
         } else {
             this.colorTarget.bind();
             if (!this.packedDepth) {
@@ -186,22 +235,23 @@ export class DrawPass {
         renderer.clear(true);
         renderer.renderBlendedOpaque(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.clear(false);
-            renderer.renderDepth(scene.primitives, camera, null);
-            this.colorTarget.bind();
-        }
-
-        // do direct-volume rendering
         if (!toDrawingBuffer) {
+            // do a depth pass if not rendering to drawing buffer and
+            // extensions.depthTexture is unsupported (i.e. depthTarget is set)
+            if (this.depthTargetPrimitives) {
+                this.depthTargetPrimitives.bind();
+                renderer.clear(false);
+                // TODO: this should only render opaque
+                renderer.renderDepth(scene.primitives, camera, null);
+                this.colorTarget.bind();
+            }
+
+            // do direct-volume rendering
             if (!this.packedDepth) {
                 this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
                 renderer.clearDepth(); // from previous frame
             }
-            renderer.renderBlendedVolume(scene.volumes, camera, this.depthTexturePrimitives);
+            renderer.renderBlendedVolumeOpaque(scene.volumes, camera, this.depthTexturePrimitives);
 
             // do volume depth pass if extensions.depthTexture is unsupported (i.e. depthTarget is set)
             if (this.depthTargetVolumes) {
@@ -211,29 +261,47 @@ export class DrawPass {
                 this.colorTarget.bind();
             }
 
+            // merge depths from primitive and volume rendering
+            this._depthMerge();
+            this.colorTarget.bind();
+
+            if (PostprocessingPass.isEnabled(postprocessingProps)) {
+                this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
+            }
+            renderer.renderBlendedVolumeTransparent(scene.volumes, camera, this.depthTexturePrimitives);
+
+            const target = PostprocessingPass.isEnabled(postprocessingProps)
+                ? this.postprocessing.target : this.colorTarget;
             if (!this.packedDepth) {
-                this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+                this.depthTexturePrimitives.attachFramebuffer(target.framebuffer, 'depth');
             }
+            target.bind();
         }
 
         renderer.renderBlendedTransparent(scene.primitives, camera, null);
-
-        // merge depths from primitive and volume rendering
-        if (!toDrawingBuffer) {
-            this._depthMerge();
-            this.colorTarget.bind();
-        }
     }
 
-    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean) {
+    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
+        const volumeRendering = scene.volumes.renderables.length > 0;
+        const postprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
+        const antialiasingEnabled = AntialiasingPass.isEnabled(postprocessingProps);
+
         const { x, y, width, height } = camera.viewport;
         renderer.setViewport(x, y, width, height);
         renderer.update(camera);
 
         if (this.wboitEnabled) {
-            this._renderWboit(renderer, camera, scene, toDrawingBuffer);
+            this._renderWboit(renderer, camera, scene, transparentBackground, postprocessingProps);
         } else {
-            this._renderBlended(renderer, camera, scene, toDrawingBuffer);
+            this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, transparentBackground, postprocessingProps);
+        }
+
+        if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            this.postprocessing.target.bind();
+        } else if (!toDrawingBuffer || volumeRendering || this.wboitEnabled) {
+            this.colorTarget.bind();
+        } else {
+            this.drawTarget.bind();
         }
 
         if (helper.debug.isEnabled) {
@@ -249,18 +317,40 @@ export class DrawPass {
             renderer.renderBlended(helper.camera.scene, helper.camera.camera, null);
         }
 
+        if (antialiasingEnabled) {
+            this.antialiasing.render(camera, toDrawingBuffer, postprocessingProps);
+        } else if (toDrawingBuffer) {
+            this.drawTarget.bind();
+
+            this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
+            if (PostprocessingPass.isEnabled(postprocessingProps)) {
+                this.copyFboPostprocessing.render();
+            } else if (volumeRendering || this.wboitEnabled) {
+                this.copyFboTarget.render();
+            }
+        }
+
         this.webgl.gl.flush();
     }
 
-    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {
+    render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
         renderer.setTransparentBackground(transparentBackground);
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
 
         if (StereoCamera.is(camera)) {
-            this._render(renderer, camera.left, scene, helper, toDrawingBuffer);
-            this._render(renderer, camera.right, scene, helper, toDrawingBuffer);
+            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
+            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
         } else {
-            this._render(renderer, camera, scene, helper, toDrawingBuffer);
+            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, postprocessingProps);
+        }
+    }
+
+    getColorTarget(postprocessingProps: PostprocessingProps): RenderTarget {
+        if (AntialiasingPass.isEnabled(postprocessingProps)) {
+            return this.antialiasing.target;
+        } else if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            return this.postprocessing.target;
         }
+        return this.colorTarget;
     }
 }

+ 130 - 0
src/mol-canvas3d/passes/fxaa.ts

@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { TextureSpec, UniformSpec, DefineSpec, Values } from '../../mol-gl/renderable/schema';
+import { ShaderCode } from '../../mol-gl/shader-code';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
+import { Texture } from '../../mol-gl/webgl/texture';
+import { Vec2 } from '../../mol-math/linear-algebra';
+import { ValueCell } from '../../mol-util';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import quad_vert from '../../mol-gl/shader/quad.vert';
+import fxaa_frag from '../../mol-gl/shader/fxaa.frag';
+import { Viewport } from '../camera/util';
+import { RenderTarget } from '../../mol-gl/webgl/render-target';
+
+export const FxaaParams = {
+    edgeThresholdMin: PD.Numeric(0.0312, { min: 0.0312, max: 0.0833, step: 0.0001 }, { description: 'Trims the algorithm from processing darks.' }),
+    edgeThresholdMax: PD.Numeric(0.063, { min: 0.063, max: 0.333, step: 0.001 }, { description: 'The minimum amount of local contrast required to apply algorithm.' }),
+    iterations: PD.Numeric(12, { min: 0, max: 16, step: 1 }, { description: 'Number of edge exploration steps.' }),
+    subpixelQuality: PD.Numeric(0.30, { min: 0.00, max: 1.00, step: 0.01 }, { description: 'Choose the amount of sub-pixel aliasing removal.' }),
+};
+export type FxaaProps = PD.Values<typeof FxaaParams>
+
+export class FxaaPass {
+    private readonly renderable: FxaaRenderable
+
+    constructor(private webgl: WebGLContext, input: Texture) {
+        this.renderable = getFxaaRenderable(webgl, input);
+    }
+
+    private updateState(viewport: Viewport) {
+        const { gl, state } = this.webgl;
+
+        state.enable(gl.SCISSOR_TEST);
+        state.disable(gl.BLEND);
+        state.disable(gl.DEPTH_TEST);
+        state.depthMask(false);
+
+        const { x, y, width, height } = viewport;
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+
+        state.clearColor(0, 0, 0, 1);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+    }
+
+    setSize(width: number, height: number) {
+        ValueCell.update(this.renderable.values.uTexSizeInv, Vec2.set(this.renderable.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
+    }
+
+    update(input: Texture, props: FxaaProps) {
+        const { values } = this.renderable;
+        const { edgeThresholdMin, edgeThresholdMax, iterations, subpixelQuality } = props;
+
+        let needsUpdate = false;
+
+        if (values.tColor.ref.value !== input) {
+            ValueCell.update(this.renderable.values.tColor, input);
+            needsUpdate = true;
+        }
+
+        if (values.dEdgeThresholdMin.ref.value !== edgeThresholdMin) needsUpdate = true;
+        ValueCell.updateIfChanged(values.dEdgeThresholdMin, edgeThresholdMin);
+
+        if (values.dEdgeThresholdMax.ref.value !== edgeThresholdMax) needsUpdate = true;
+        ValueCell.updateIfChanged(values.dEdgeThresholdMax, edgeThresholdMax);
+
+        if (values.dIterations.ref.value !== iterations) needsUpdate = true;
+        ValueCell.updateIfChanged(values.dIterations, iterations);
+
+        if (values.dSubpixelQuality.ref.value !== subpixelQuality) needsUpdate = true;
+        ValueCell.updateIfChanged(values.dSubpixelQuality, subpixelQuality);
+
+        if (needsUpdate) {
+            this.renderable.update();
+        }
+    }
+
+    render(viewport: Viewport, target: RenderTarget | undefined) {
+        if (target) {
+            target.bind();
+        } else {
+            this.webgl.unbindFramebuffer();
+        }
+        this.updateState(viewport);
+        this.renderable.render();
+    }
+}
+
+//
+
+const FxaaSchema = {
+    ...QuadSchema,
+    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    uTexSizeInv: UniformSpec('v2'),
+
+    dEdgeThresholdMin: DefineSpec('number'),
+    dEdgeThresholdMax: DefineSpec('number'),
+    dIterations: DefineSpec('number'),
+    dSubpixelQuality: DefineSpec('number'),
+};
+const FxaaShaderCode = ShaderCode('fxaa', quad_vert, fxaa_frag);
+type FxaaRenderable = ComputeRenderable<Values<typeof FxaaSchema>>
+
+function getFxaaRenderable(ctx: WebGLContext, colorTexture: Texture): FxaaRenderable {
+    const width = colorTexture.getWidth();
+    const height = colorTexture.getHeight();
+
+    const values: Values<typeof FxaaSchema> = {
+        ...QuadValues,
+        tColor: ValueCell.create(colorTexture),
+        uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
+
+        dEdgeThresholdMin: ValueCell.create(0.0312),
+        dEdgeThresholdMax: ValueCell.create(0.125),
+        dIterations: ValueCell.create(12),
+        dSubpixelQuality: ValueCell.create(0.3),
+    };
+
+    const schema = { ...FxaaSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', FxaaShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}

+ 4 - 12
src/mol-canvas3d/passes/image.ts

@@ -10,7 +10,7 @@ import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { DrawPass } from './draw';
-import { PostprocessingPass, PostprocessingParams } from './postprocessing';
+import { PostprocessingParams } from './postprocessing';
 import { MultiSamplePass, MultiSampleParams, MultiSampleHelper } from './multi-sample';
 import { Camera } from '../camera';
 import { Viewport } from '../camera/util';
@@ -38,7 +38,6 @@ export class ImagePass {
     get colorTarget() { return this._colorTarget; }
 
     private readonly drawPass: DrawPass
-    private readonly postprocessingPass: PostprocessingPass
     private readonly multiSamplePass: MultiSamplePass
     private readonly multiSampleHelper: MultiSampleHelper
     private readonly helper: Helper
@@ -50,8 +49,7 @@ export class ImagePass {
         this.props = { ...PD.getDefaultValues(ImageParams), ...props };
 
         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.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
         this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
 
         this.helper = {
@@ -70,7 +68,6 @@ export class ImagePass {
         this._height = height;
 
         this.drawPass.setSize(width, height);
-        this.postprocessingPass.syncSize();
         this.multiSamplePass.syncSize();
     }
 
@@ -88,13 +85,8 @@ export class ImagePass {
             this.multiSampleHelper.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props);
             this._colorTarget = this.multiSamplePass.colorTarget;
         } else {
-            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground);
-            if (PostprocessingPass.isEnabled(this.props.postprocessing)) {
-                this.postprocessingPass.render(this._camera, false, this.props.postprocessing);
-                this._colorTarget = this.postprocessingPass.target;
-            } else {
-                this._colorTarget = this.drawPass.colorTarget;
-            }
+            this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props.postprocessing);
+            this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
         }
     }
 

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

@@ -16,7 +16,7 @@ import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/rendera
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { Camera } from '../../mol-canvas3d/camera';
-import { PostprocessingPass, PostprocessingProps } from './postprocessing';
+import { PostprocessingProps } from './postprocessing';
 import { DrawPass } from './draw';
 import Renderer from '../../mol-gl/renderer';
 import Scene from '../../mol-gl/scene';
@@ -68,7 +68,7 @@ export class MultiSamplePass {
     private holdTarget: RenderTarget
     private compose: ComposeRenderable
 
-    constructor(private webgl: WebGLContext, private drawPass: DrawPass, private postprocessing: PostprocessingPass) {
+    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
         const { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } = webgl.extensions;
         const width = drawPass.colorTarget.getWidth();
         const height = drawPass.colorTarget.getHeight();
@@ -111,7 +111,7 @@ export class MultiSamplePass {
     }
 
     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 { compose, composeTarget, drawPass, webgl } = this;
         const { gl, state } = webgl;
 
         // based on the Multisample Anti-Aliasing Render Pass
@@ -125,10 +125,8 @@ export class MultiSamplePass {
         const baseSampleWeight = 1.0 / offsetList.length;
         const roundingRange = 1 / 32;
 
-        const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
-
         camera.viewOffset.enabled = true;
-        ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+        ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
         compose.update();
 
         // render the scene multiple times, each slightly jitter offset
@@ -145,9 +143,8 @@ export class MultiSamplePass {
             const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution;
             ValueCell.update(compose.values.uWeight, sampleWeight);
 
-            // render scene and optionally postprocess
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
-            if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
+            // render scene
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
 
             // compose rendered scene with compose target
             composeTarget.bind();
@@ -181,7 +178,7 @@ export class MultiSamplePass {
     }
 
     private renderTemporalMultiSample(sampleIndex: number, renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
-        const { compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this;
+        const { compose, composeTarget, holdTarget, drawPass, webgl } = this;
         const { gl, state } = webgl;
 
         // based on the Multisample Anti-Aliasing Render Pass
@@ -195,13 +192,11 @@ export class MultiSamplePass {
 
         const { x, y, width, height } = camera.viewport;
         const sampleWeight = 1.0 / offsetList.length;
-        const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
 
         if (sampleIndex === -1) {
-            drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
-            if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
+            drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
             ValueCell.update(compose.values.uWeight, 1.0);
-            ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+            ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             compose.update();
 
             holdTarget.bind();
@@ -214,7 +209,7 @@ export class MultiSamplePass {
             sampleIndex += 1;
         } else {
             camera.viewOffset.enabled = true;
-            ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
+            ValueCell.update(compose.values.tColor, drawPass.getColorTarget(props.postprocessing).texture);
             ValueCell.update(compose.values.uWeight, sampleWeight);
             compose.update();
 
@@ -226,9 +221,8 @@ export class MultiSamplePass {
                 Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
                 camera.update();
 
-                // render scene and optionally postprocess
-                drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
-                if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
+                // render scene
+                drawPass.render(renderer, camera, scene, helper, false, transparentBackground, props.postprocessing);
 
                 // compose rendered scene with compose target
                 composeTarget.bind();

+ 1 - 5
src/mol-canvas3d/passes/passes.ts

@@ -6,29 +6,25 @@
 
 import { DrawPass } from './draw';
 import { PickPass } from './pick';
-import { PostprocessingPass } from './postprocessing';
 import { MultiSamplePass } from './multi-sample';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 
 export class Passes {
     readonly draw: DrawPass
     readonly pick: PickPass
-    readonly postprocessing: PostprocessingPass
     readonly multiSample: MultiSamplePass
 
     constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) {
         const { gl } = webgl;
         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);
+        this.multiSample = new MultiSamplePass(webgl, this.draw);
     }
 
     updateSize() {
         const { gl } = this.webgl;
         this.draw.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight);
         this.pick.syncSize();
-        this.postprocessing.syncSize();
         this.multiSample.syncSize();
     }
 }

+ 407 - 163
src/mol-canvas3d/passes/postprocessing.ts

@@ -2,6 +2,7 @@
  * 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 { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
@@ -12,20 +13,174 @@ import { Texture } from '../../mol-gl/webgl/texture';
 import { ValueCell } from '../../mol-util';
 import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
-import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { DrawPass } from './draw';
-import { Camera, ICamera } from '../../mol-canvas3d/camera';
+import { ICamera } from '../../mol-canvas3d/camera';
 import quad_vert from '../../mol-gl/shader/quad.vert';
+import outlines_frag from '../../mol-gl/shader/outlines.frag';
+import ssao_frag from '../../mol-gl/shader/ssao.frag';
+import ssao_blur_frag from '../../mol-gl/shader/ssao-blur.frag';
 import postprocessing_frag from '../../mol-gl/shader/postprocessing.frag';
-import fxaa_frag from '../../mol-gl/shader/fxaa.frag';
-import { StereoCamera } from '../camera/stereo';
+import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
+import { Color } from '../../mol-util/color';
+import { FxaaParams, FxaaPass } from './fxaa';
+import { SmaaParams, SmaaPass } from './smaa';
+
+const OutlinesSchema = {
+    ...QuadSchema,
+    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+
+    dOrthographic: DefineSpec('number'),
+    uNear: UniformSpec('f'),
+    uFar: UniformSpec('f'),
+
+    uMaxPossibleViewZDiff: UniformSpec('f'),
+};
+type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
+
+function getOutlinesRenderable(ctx: WebGLContext, depthTexture: Texture): OutlinesRenderable {
+    const values: Values<typeof OutlinesSchema> = {
+        ...QuadValues,
+        tDepth: ValueCell.create(depthTexture),
+        uTexSize: ValueCell.create(Vec2.create(depthTexture.getWidth(), depthTexture.getHeight())),
+
+        dOrthographic: ValueCell.create(0),
+        uNear: ValueCell.create(1),
+        uFar: ValueCell.create(10000),
+
+        uMaxPossibleViewZDiff: ValueCell.create(0.5),
+    };
+
+    const schema = { ...OutlinesSchema };
+    const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+const SsaoSchema = {
+    ...QuadSchema,
+    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+
+    uSamples: UniformSpec('v3[]'),
+    dNSamples: DefineSpec('number'),
+
+    uProjection: UniformSpec('m4'),
+    uInvProjection: UniformSpec('m4'),
+
+    uTexSize: UniformSpec('v2'),
+
+    uRadius: UniformSpec('f'),
+    uBias: UniformSpec('f'),
+};
+
+type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
+
+function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRenderable {
+    const values: Values<typeof SsaoSchema> = {
+        ...QuadValues,
+        tDepth: ValueCell.create(depthTexture),
+
+        uSamples: ValueCell.create([0.0, 0.0, 1.0]),
+        dNSamples: ValueCell.create(1),
+
+        uProjection: ValueCell.create(Mat4.identity()),
+        uInvProjection: ValueCell.create(Mat4.identity()),
+
+        uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
+
+        uRadius: ValueCell.create(8.0),
+        uBias: ValueCell.create(0.025),
+    };
+
+    const schema = { ...SsaoSchema };
+    const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+const SsaoBlurSchema = {
+    ...QuadSchema,
+    tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+
+    uKernel: UniformSpec('f[]'),
+    dOcclusionKernelSize: DefineSpec('number'),
+
+    uBlurDirectionX: UniformSpec('f'),
+    uBlurDirectionY: UniformSpec('f'),
+
+    uMaxPossibleViewZDiff: UniformSpec('f'),
+
+    uNear: UniformSpec('f'),
+    uFar: UniformSpec('f'),
+    dOrthographic: DefineSpec('number'),
+};
+
+type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
+
+function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
+    const values: Values<typeof SsaoBlurSchema> = {
+        ...QuadValues,
+        tSsaoDepth: ValueCell.create(ssaoDepthTexture),
+        uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
+
+        uKernel: ValueCell.create([0.0]),
+        dOcclusionKernelSize: ValueCell.create(1),
+
+        uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
+        uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
+
+        uMaxPossibleViewZDiff: ValueCell.create(0.5),
+
+        uNear: ValueCell.create(0.0),
+        uFar: ValueCell.create(10000.0),
+        dOrthographic: ValueCell.create(0),
+    };
+
+    const schema = { ...SsaoBlurSchema };
+    const shaderCode = ShaderCode('ssao_blur', quad_vert, ssao_blur_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+function getBlurKernel(kernelSize: number): number[] {
+    let sigma = kernelSize / 3.0;
+    let halfKernelSize = Math.floor((kernelSize + 1) / 2);
+
+    let kernel = [];
+    for (let x = 0; x < halfKernelSize; x++) {
+        kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
+    }
+
+    return kernel;
+}
+
+function getSamples(vectorSamples: Vec3[], nSamples: number): number[] {
+    let samples = [];
+    for (let i = 0; i < nSamples; i++) {
+        let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
+        scale = 0.1 + scale * (1.0 - 0.1);
+
+        samples.push(vectorSamples[i][0] * scale);
+        samples.push(vectorSamples[i][1] * scale);
+        samples.push(vectorSamples[i][2] * scale);
+    }
+
+    return samples;
+}
 
 const PostprocessingSchema = {
     ...QuadSchema,
+    tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
-    tPackedDepth: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
+    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     uTexSize: UniformSpec('v2'),
 
     dOrthographic: DefineSpec('number'),
@@ -34,28 +189,26 @@ const PostprocessingSchema = {
     uFogNear: UniformSpec('f'),
     uFogFar: UniformSpec('f'),
     uFogColor: UniformSpec('v3'),
+    uTransparentBackground: UniformSpec('b'),
+
+    uMaxPossibleViewZDiff: UniformSpec('f'),
 
     dOcclusionEnable: DefineSpec('boolean'),
-    dOcclusionKernelSize: DefineSpec('number'),
-    uOcclusionBias: UniformSpec('f'),
-    uOcclusionRadius: UniformSpec('f'),
 
     dOutlineEnable: DefineSpec('boolean'),
-    uOutlineScale: UniformSpec('f'),
+    dOutlineScale: DefineSpec('number'),
     uOutlineThreshold: UniformSpec('f'),
 };
-const PostprocessingShaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag);
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
 
-function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture): PostprocessingRenderable {
-    const width = colorTexture.getWidth();
-    const height = colorTexture.getHeight();
-
+function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture): PostprocessingRenderable {
     const values: Values<typeof PostprocessingSchema> = {
         ...QuadValues,
+        tSsaoDepth: ValueCell.create(ssaoDepthTexture),
         tColor: ValueCell.create(colorTexture),
-        tPackedDepth: ValueCell.create(depthTexture),
-        uTexSize: ValueCell.create(Vec2.create(width, height)),
+        tDepth: ValueCell.create(depthTexture),
+        tOutlines: ValueCell.create(outlinesTexture),
+        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
 
         dOrthographic: ValueCell.create(0),
         uNear: ValueCell.create(1),
@@ -63,177 +216,255 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         uFogNear: ValueCell.create(10000),
         uFogFar: ValueCell.create(10000),
         uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
+        uTransparentBackground: ValueCell.create(false),
+
+        uMaxPossibleViewZDiff: ValueCell.create(0.5),
 
         dOcclusionEnable: ValueCell.create(false),
-        dOcclusionKernelSize: ValueCell.create(4),
-        uOcclusionBias: ValueCell.create(0.5),
-        uOcclusionRadius: ValueCell.create(64),
 
         dOutlineEnable: ValueCell.create(false),
-        uOutlineScale: ValueCell.create(1 * ctx.pixelRatio),
-        uOutlineThreshold: ValueCell.create(0.8),
+        dOutlineScale: ValueCell.create(1),
+        uOutlineThreshold: ValueCell.create(0.33),
     };
 
     const schema = { ...PostprocessingSchema };
-    const renderItem = createComputeRenderItem(ctx, 'triangles', PostprocessingShaderCode, schema, values);
+    const shaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
 
     return createComputeRenderable(renderItem, values);
 }
 
 export const PostprocessingParams = {
-    occlusion: PD.MappedStatic('off', {
+    occlusion: PD.MappedStatic('on', {
         on: PD.Group({
-            kernelSize: PD.Numeric(4, { min: 1, max: 32, step: 1 }),
-            bias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
-            radius: PD.Numeric(64, { min: 0, max: 256, step: 1 }),
+            samples: PD.Numeric(64, {min: 1, max: 256, step: 1}),
+            radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final radius is 2^x.' }),
+            bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
+            blurKernelSize: PD.Numeric(20, { min: 1, max: 25, step: 2 }),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
     outline: PD.MappedStatic('off', {
         on: PD.Group({
-            scale: PD.Numeric(1, { min: 0, max: 10, step: 1 }),
-            threshold: PD.Numeric(0.8, { min: 0, max: 5, step: 0.01 }),
+            scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
+            threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Draw outline around 3D objects' }),
-    antialiasing: PD.MappedStatic('on', {
-        on: PD.Group({
-            edgeThresholdMin:PD.Numeric(0.0312, { min: 0.0312, max: 0.0833, step: 0.0001 }, { description: 'Trims the algorithm from processing darks.' }),
-            edgeThresholdMax: PD.Numeric(0.063, { min: 0.063, max: 0.333, step: 0.001 }, { description: 'The minimum amount of local contrast required to apply algorithm.' }),
-            iterations: PD.Numeric(12, { min: 0, max: 32, step: 1 }, { description: 'Number of edge exploration steps.' }),
-            subpixelQuality: PD.Numeric(1.00, { min: 0.00, max: 1.00, step: 0.01 }, { description: 'Choose the amount of sub-pixel aliasing removal.' }),
-        }),
+    antialiasing: PD.MappedStatic('smaa', {
+        fxaa: PD.Group(FxaaParams),
+        smaa: PD.Group(SmaaParams),
         off: PD.Group({})
-    }, { cycle: true, description: 'Fast Approximate Anti-Aliasing (FXAA)' }),
+    }, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
 };
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
 export class PostprocessingPass {
     static isEnabled(props: PostprocessingProps) {
-        return props.occlusion.name === 'on' || props.outline.name === 'on' || props.antialiasing.name === 'on';
+        return props.occlusion.name === 'on' || props.outline.name === 'on';
     }
 
     readonly target: RenderTarget
 
-    private readonly tmpTarget: RenderTarget
+    private readonly outlinesTarget: RenderTarget
+    private readonly outlinesRenderable: OutlinesRenderable
+
+    private readonly randomHemisphereVector: Vec3[]
+    private readonly ssaoFramebuffer: Framebuffer
+    private readonly ssaoBlurFirstPassFramebuffer: Framebuffer
+    private readonly ssaoBlurSecondPassFramebuffer: Framebuffer
+
+    private readonly ssaoDepthTexture: Texture
+    private readonly ssaoDepthBlurProxyTexture: Texture
+
+    private readonly ssaoRenderable: SsaoRenderable
+    private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable
+    private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable
+
+    private nSamples: number
+    private blurKernelSize: number
+
     private readonly renderable: PostprocessingRenderable
-    private readonly fxaa: FxaaRenderable
 
-    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
+    constructor(private webgl: WebGLContext, drawPass: DrawPass) {
         const { colorTarget, depthTexture } = drawPass;
         const width = colorTarget.getWidth();
         const height = colorTarget.getHeight();
 
-        this.target = webgl.createRenderTarget(width, height, false);
-        this.tmpTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
-        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture);
-        this.fxaa = getFxaaRenderable(webgl, this.tmpTarget.texture);
-    }
+        this.nSamples = 1;
+        this.blurKernelSize = 1;
+
+        this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
 
-    syncSize() {
-        const width = this.drawPass.colorTarget.getWidth();
-        const height = this.drawPass.colorTarget.getHeight();
+        this.outlinesTarget = webgl.createRenderTarget(width, height, false);
+        this.outlinesRenderable = getOutlinesRenderable(webgl, depthTexture);
 
+        this.randomHemisphereVector = [];
+        for (let i = 0; i < 256; i++) {
+            let v = Vec3();
+            v[0] = Math.random() * 2.0 - 1.0;
+            v[1] = Math.random() * 2.0 - 1.0;
+            v[2] = Math.random();
+            Vec3.normalize(v, v);
+            Vec3.scale(v, v, Math.random());
+            this.randomHemisphereVector.push(v);
+        }
+        this.ssaoFramebuffer = webgl.resources.framebuffer();
+        this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
+        this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
+
+        this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        this.ssaoDepthTexture.define(width, height);
+        this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
+
+        this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
+        this.ssaoDepthBlurProxyTexture.define(width, height);
+        this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
+
+        this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
+
+        this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture);
+        this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
+        this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
+        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture,  depthTexture, this.outlinesTarget.texture, this.ssaoDepthTexture);
+    }
+
+    setSize(width: number, height: number) {
         const [w, h] = this.renderable.values.uTexSize.ref.value;
         if (width !== w || height !== h) {
             this.target.setSize(width, height);
-            this.tmpTarget.setSize(width, height);
+            this.outlinesTarget.setSize(width, height);
+            this.ssaoDepthTexture.define(width, height);
+            this.ssaoDepthBlurProxyTexture.define(width, height);
+
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
-            ValueCell.update(this.fxaa.values.uTexSizeInv, Vec2.set(this.fxaa.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
+            ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, width, height));
         }
     }
 
-    private updateState(camera: ICamera) {
-        const { gl, state } = this.webgl;
+    private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
+        let needsUpdateMain = false;
+        let needsUpdateSsao = false;
+        let needsUpdateSsaoBlur = false;
 
-        state.enable(gl.SCISSOR_TEST);
-        state.disable(gl.BLEND);
-        state.disable(gl.DEPTH_TEST);
-        state.depthMask(false);
+        const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
+        const outlinesEnabled = props.outline.name === 'on';
+        const occlusionEnabled = props.occlusion.name === 'on';
 
-        const { x, y, width, height } = camera.viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        let invProjection = Mat4.identity();
+        Mat4.invert(invProjection, camera.projection);
 
-        state.clearColor(0, 0, 0, 1);
-        gl.clear(gl.COLOR_BUFFER_BIT);
-    }
+        if (props.occlusion.name === 'on') {
+            ValueCell.updateIfChanged(this.ssaoRenderable.values.uProjection, camera.projection);
+            ValueCell.updateIfChanged(this.ssaoRenderable.values.uInvProjection, invProjection);
 
-    private _renderPostprocessing(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
-        const { values } = this.renderable;
+            ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
+            ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
 
-        ValueCell.updateIfChanged(values.uFar, camera.far);
-        ValueCell.updateIfChanged(values.uNear, camera.near);
-        ValueCell.updateIfChanged(values.uFogFar, camera.fogFar);
-        ValueCell.updateIfChanged(values.uFogNear, camera.fogNear);
+            ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
+            ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
 
-        let needsUpdate = false;
+            if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateSsaoBlur = true; }
+            ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
+            ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
 
-        const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
-        if (values.dOrthographic.ref.value !== orthographic) needsUpdate = true;
-        ValueCell.updateIfChanged(values.dOrthographic, orthographic);
+            if (this.nSamples !== props.occlusion.params.samples) {
+                needsUpdateSsao = true;
+
+                this.nSamples = props.occlusion.params.samples;
+                ValueCell.updateIfChanged(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
+                ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
+            }
+            ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
+            ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
+
+            if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
+                needsUpdateSsaoBlur = true;
+
+                this.blurKernelSize = props.occlusion.params.blurKernelSize;
+                let kernel = getBlurKernel(this.blurKernelSize);
+
+                ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
+                ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
+                ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
+                ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
+            }
 
-        const occlusion = props.occlusion.name === 'on';
-        if (values.dOcclusionEnable.ref.value !== occlusion) needsUpdate = true;
-        ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusion);
-        if (props.occlusion.name === 'on') {
-            const { kernelSize } = props.occlusion.params;
-            if (values.dOcclusionKernelSize.ref.value !== kernelSize) needsUpdate = true;
-            ValueCell.updateIfChanged(values.dOcclusionKernelSize, kernelSize);
-            ValueCell.updateIfChanged(values.uOcclusionBias, props.occlusion.params.bias);
-            ValueCell.updateIfChanged(values.uOcclusionRadius, props.occlusion.params.radius);
         }
 
-        const outline = props.outline.name === 'on';
-        if (values.dOutlineEnable.ref.value !== outline) needsUpdate = true;
-        ValueCell.updateIfChanged(values.dOutlineEnable, outline);
         if (props.outline.name === 'on') {
-            ValueCell.updateIfChanged(values.uOutlineScale, props.outline.params.scale * this.webgl.pixelRatio);
-            ValueCell.updateIfChanged(values.uOutlineThreshold, props.outline.params.threshold);
+            const factor = Math.pow(1000, props.outline.params.threshold) / 1000;
+            const maxPossibleViewZDiff = factor * (camera.far - camera.near);
+            const outlineScale = props.outline.params.scale - 1;
+
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
+
+            ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
+            ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, props.outline.params.threshold);
+            if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
+            ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
         }
 
-        if (needsUpdate) {
-            this.renderable.update();
+        ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
+        ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
+        ValueCell.updateIfChanged(this.renderable.values.uFogFar, camera.fogFar);
+        ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
+        ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor));
+        ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground);
+        if (this.renderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateMain = true; }
+        ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
+        if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) { needsUpdateMain = true; }
+        ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, outlinesEnabled);
+        if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
+        ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
+
+        if (needsUpdateSsao) {
+            this.ssaoRenderable.update();
         }
 
-        if (props.antialiasing.name === 'on') {
-            this.tmpTarget.bind();
-        } else if (toDrawingBuffer) {
-            this.webgl.unbindFramebuffer();
-        } else {
-            this.target.bind();
+        if (needsUpdateSsaoBlur) {
+            this.ssaoBlurFirstPassRenderable.update();
+            this.ssaoBlurSecondPassRenderable.update();
         }
 
-        this.updateState(camera);
-        this.renderable.render();
-    }
+        if (needsUpdateMain) {
+            this.renderable.update();
+        }
 
-    private _renderFxaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
-        if (props.antialiasing.name === 'off') return;
+        const { gl, state } = this.webgl;
 
-        const { values } = this.fxaa;
+        state.enable(gl.SCISSOR_TEST);
+        state.disable(gl.BLEND);
+        state.disable(gl.DEPTH_TEST);
+        state.depthMask(false);
+
+        const { x, y, width, height } = camera.viewport;
+        gl.viewport(x, y, width, height);
+        gl.scissor(x, y, width, height);
+    }
 
-        let needsUpdate = false;
+    render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
+        this.updateState(camera, transparentBackground, backgroundColor, props);
 
-        const input = (props.occlusion.name === 'on' || props.outline.name === 'on')
-            ? this.tmpTarget.texture : this.drawPass.colorTarget.texture;
-        if (values.tColor.ref.value !== input) {
-            ValueCell.update(this.fxaa.values.tColor, input);
-            needsUpdate = true;
+        if (props.outline.name === 'on') {
+            this.outlinesTarget.bind();
+            this.outlinesRenderable.render();
         }
 
-        const { edgeThresholdMin, edgeThresholdMax, iterations, subpixelQuality } = props.antialiasing.params;
-        if (values.dEdgeThresholdMin.ref.value !== edgeThresholdMin) needsUpdate = true;
-        ValueCell.updateIfChanged(values.dEdgeThresholdMin, edgeThresholdMin);
-        if (values.dEdgeThresholdMax.ref.value !== edgeThresholdMax) needsUpdate = true;
-        ValueCell.updateIfChanged(values.dEdgeThresholdMax, edgeThresholdMax);
-        if (values.dIterations.ref.value !== iterations) needsUpdate = true;
-        ValueCell.updateIfChanged(values.dIterations, iterations);
-        if (values.dSubpixelQuality.ref.value !== subpixelQuality) needsUpdate = true;
-        ValueCell.updateIfChanged(values.dSubpixelQuality, subpixelQuality);
-
-        if (needsUpdate) {
-            this.fxaa.update();
+        if (props.occlusion.name === 'on') {
+            this.ssaoFramebuffer.bind();
+            this.ssaoRenderable.render();
+
+            this.ssaoBlurFirstPassFramebuffer.bind();
+            this.ssaoBlurFirstPassRenderable.render();
+
+            this.ssaoBlurSecondPassFramebuffer.bind();
+            this.ssaoBlurSecondPassRenderable.render();
         }
 
         if (toDrawingBuffer) {
@@ -242,62 +473,75 @@ export class PostprocessingPass {
             this.target.bind();
         }
 
-        this.updateState(camera);
-        this.fxaa.render();
-    }
+        const { gl, state } = this.webgl;
+        state.clearColor(0, 0, 0, 1);
+        gl.clear(gl.COLOR_BUFFER_BIT);
 
-    private _render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
-        if (props.occlusion.name === 'on' || props.outline.name === 'on' || props.antialiasing.name === 'off') {
-            this._renderPostprocessing(camera, toDrawingBuffer, props);
-        }
+        this.renderable.render();
+    }
+}
 
-        if (props.antialiasing.name === 'on') {
-            this._renderFxaa(camera, toDrawingBuffer, props);
-        }
+export class AntialiasingPass {
+    static isEnabled(props: PostprocessingProps) {
+        return props.antialiasing.name !== 'off';
     }
 
-    render(camera: Camera | StereoCamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
-        if (StereoCamera.is(camera)) {
-            this._render(camera.left, toDrawingBuffer, props);
-            this._render(camera.right, toDrawingBuffer, props);
-        } else {
-            this._render(camera, toDrawingBuffer, props);
-        }
+    readonly target: RenderTarget
+    private readonly fxaa: FxaaPass
+    private readonly smaa: SmaaPass
+
+    constructor(webgl: WebGLContext, private drawPass: DrawPass) {
+        const { colorTarget } = drawPass;
+        const width = colorTarget.getWidth();
+        const height = colorTarget.getHeight();
+
+        this.target = webgl.createRenderTarget(width, height, false);
+        this.fxaa = new FxaaPass(webgl, this.target.texture);
+        this.smaa = new SmaaPass(webgl, this.target.texture);
     }
-}
 
-//
+    setSize(width: number, height: number) {
+        const w = this.target.texture.getWidth();
+        const h = this.target.texture.getHeight();
 
-const FxaaSchema = {
-    ...QuadSchema,
-    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
-    uTexSizeInv: UniformSpec('v2'),
+        if (width !== w || height !== h) {
+            this.target.setSize(width, height);
+            this.fxaa.setSize(width, height);
+            if (this.smaa.supported) this.smaa.setSize(width, height);
+        }
+    }
 
-    dEdgeThresholdMin: DefineSpec('number'),
-    dEdgeThresholdMax: DefineSpec('number'),
-    dIterations: DefineSpec('number'),
-    dSubpixelQuality: DefineSpec('number'),
-};
-const FxaaShaderCode = ShaderCode('fxaa', quad_vert, fxaa_frag);
-type FxaaRenderable = ComputeRenderable<Values<typeof FxaaSchema>>
+    private _renderFxaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        if (props.antialiasing.name !== 'fxaa') return;
 
-function getFxaaRenderable(ctx: WebGLContext, colorTexture: Texture): FxaaRenderable {
-    const width = colorTexture.getWidth();
-    const height = colorTexture.getHeight();
+        const input = PostprocessingPass.isEnabled(props)
+            ? this.drawPass.postprocessing.target.texture
+            : this.drawPass.colorTarget.texture;
+        this.fxaa.update(input, props.antialiasing.params);
+        this.fxaa.render(camera.viewport, toDrawingBuffer ? undefined : this.target);
+    }
 
-    const values: Values<typeof FxaaSchema> = {
-        ...QuadValues,
-        tColor: ValueCell.create(colorTexture),
-        uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
+    private _renderSmaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        if (props.antialiasing.name !== 'smaa') return;
 
-        dEdgeThresholdMin: ValueCell.create(0.0312),
-        dEdgeThresholdMax: ValueCell.create(0.125),
-        dIterations: ValueCell.create(12),
-        dSubpixelQuality: ValueCell.create(0.75),
-    };
+        const input = PostprocessingPass.isEnabled(props)
+            ? this.drawPass.postprocessing.target.texture
+            : this.drawPass.colorTarget.texture;
+        this.smaa.update(input, props.antialiasing.params);
+        this.smaa.render(camera.viewport, toDrawingBuffer ? undefined : this.target);
+    }
 
-    const schema = { ...FxaaSchema };
-    const renderItem = createComputeRenderItem(ctx, 'triangles', FxaaShaderCode, schema, values);
+    render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        if (props.antialiasing.name === 'off') return;
 
-    return createComputeRenderable(renderItem, values);
+        if (props.antialiasing.name === 'fxaa') {
+            this._renderFxaa(camera, toDrawingBuffer, props);
+        } else if (props.antialiasing.name === 'smaa') {
+            if (!this.smaa.supported) {
+                throw new Error('SMAA not supported, missing "HTMLImageElement"');
+            }
+            this._renderSmaa(camera, toDrawingBuffer, props);
+        }
+    }
 }
+

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 249 - 0
src/mol-canvas3d/passes/smaa.ts


+ 14 - 6
src/mol-canvas3d/passes/wboit.ts

@@ -51,9 +51,9 @@ export class WboitPass {
     private readonly textureA: Texture
     private readonly textureB: Texture
 
-    private _enabled = false;
-    get enabled() {
-        return this._enabled;
+    private _supported = false;
+    get supported() {
+        return this._supported;
     }
 
     bind() {
@@ -90,7 +90,7 @@ export class WboitPass {
     }
 
     reset() {
-        if (this._enabled) this._init();
+        if (this._supported) this._init();
     }
 
     private _init() {
@@ -109,7 +109,14 @@ export class WboitPass {
     static isSupported(webgl: WebGLContext) {
         const { extensions: { drawBuffers, textureFloat, colorBufferFloat, depthTexture } } = webgl;
         if (!textureFloat || !colorBufferFloat || !depthTexture || !drawBuffers) {
-            if (isDebugMode) console.log('Missing extensions required for "wboit"');
+            if (isDebugMode) {
+                const missing: string[] = [];
+                if (!textureFloat) missing.push('textureFloat');
+                if (!colorBufferFloat) missing.push('colorBufferFloat');
+                if (!depthTexture) missing.push('depthTexture');
+                if (!drawBuffers) missing.push('drawBuffers');
+                console.log(`Missing "${missing.join('", "')}" extensions required for "wboit"`);
+            }
             return false;
         } else {
             return true;
@@ -129,7 +136,8 @@ export class WboitPass {
 
         this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
         this.framebuffer = resources.framebuffer();
+
+        this._supported = true;
         this._init();
-        this._enabled = true;
     }
 }

+ 102 - 0
src/mol-geo/geometry/cylinders/cylinders-builder.ts

@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ChunkedArray } from '../../../mol-data/util';
+import { Cylinders } from './cylinders';
+import { Vec3 } from '../../../mol-math/linear-algebra';
+
+export interface CylindersBuilder {
+    add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
+    addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
+    addFixedLengthDashes(start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
+    getCylinders(): Cylinders
+}
+
+const tmpVecA = Vec3();
+const tmpVecB = Vec3();
+const tmpDir = Vec3();
+
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const caAdd = ChunkedArray.add;
+const caAdd3 = ChunkedArray.add3;
+
+export namespace CylindersBuilder {
+    export function create(initialCount = 2048, chunkSize = 1024, cylinders?: Cylinders): CylindersBuilder {
+        const groups = ChunkedArray.create(Float32Array, 1, chunkSize, cylinders ? cylinders.groupBuffer.ref.value : initialCount);
+        const starts = ChunkedArray.create(Float32Array, 3, chunkSize, cylinders ? cylinders.startBuffer.ref.value : initialCount);
+        const ends = ChunkedArray.create(Float32Array, 3, chunkSize, cylinders ? cylinders.endBuffer.ref.value : initialCount);
+        const scales = ChunkedArray.create(Float32Array, 1, chunkSize, cylinders ? cylinders.scaleBuffer.ref.value : initialCount);
+        const caps = ChunkedArray.create(Float32Array, 1, chunkSize, cylinders ? cylinders.capBuffer.ref.value : initialCount);
+
+        const add = (startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
+            for (let i = 0; i < 6; ++i) {
+                caAdd3(starts, startX, startY, startZ);
+                caAdd3(ends, endX, endY, endZ);
+                caAdd(groups, group);
+                caAdd(scales, radiusScale);
+                caAdd(caps, (topCap ? 1 : 0) + (bottomCap ? 2 : 0));
+            }
+        };
+
+        const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
+            const d = Vec3.distance(start, end);
+            const s = Math.floor(segmentCount / 2);
+            const step = 1 / segmentCount;
+
+            Vec3.sub(tmpDir, end, start);
+            for (let j = 0; j < s; ++j) {
+                const f = step * (j * 2 + 1);
+                Vec3.setMagnitude(tmpDir, tmpDir, d * f);
+                Vec3.add(tmpVecA, start, tmpDir);
+                Vec3.setMagnitude(tmpDir, tmpDir, d * step * ((j + 1) * 2));
+                Vec3.add(tmpVecB, start, tmpDir);
+                add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], radiusScale, topCap, bottomCap, group);
+            }
+        };
+
+        return {
+            add,
+            addFixedCountDashes,
+            addFixedLengthDashes: (start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
+                const d = Vec3.distance(start, end);
+                addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, group);
+            },
+            getCylinders: () => {
+                const cylinderCount = groups.elementCount / 6;
+                const gb = ChunkedArray.compact(groups, true) as Float32Array;
+                const sb = ChunkedArray.compact(starts, true) as Float32Array;
+                const eb = ChunkedArray.compact(ends, true) as Float32Array;
+                const ab = ChunkedArray.compact(scales, true) as Float32Array;
+                const cb = ChunkedArray.compact(caps, true) as Float32Array;
+                const mb = cylinders && cylinderCount <= cylinders.cylinderCount ? cylinders.mappingBuffer.ref.value : new Float32Array(cylinderCount * 18);
+                const ib = cylinders && cylinderCount <= cylinders.cylinderCount ? cylinders.indexBuffer.ref.value : new Uint32Array(cylinderCount * 12);
+                if (!cylinders || cylinderCount > cylinders.cylinderCount) fillMappingAndIndices(cylinderCount, mb, ib);
+                return Cylinders.create(mb, ib, gb, sb, eb, ab, cb, cylinderCount, cylinders);
+            }
+        };
+    }
+}
+
+function fillMappingAndIndices(n: number, mb: Float32Array, ib: Uint32Array) {
+    for (let i = 0; i < n; ++i) {
+        const mo = i * 18;
+        mb[mo] = -1; mb[mo + 1] = 1; mb[mo + 2] = -1;
+        mb[mo + 3] = -1; mb[mo + 4] = -1; mb[mo + 5] = -1;
+        mb[mo + 6] = 1; mb[mo + 7] = 1; mb[mo + 8] = -1;
+        mb[mo + 9] = 1; mb[mo + 10] = 1; mb[mo + 11] = 1;
+        mb[mo + 12] = 1; mb[mo + 13] = -1; mb[mo + 14] = -1;
+        mb[mo + 15] = 1; mb[mo + 16] = -1; mb[mo + 17] = 1;
+    }
+
+    for (let i = 0; i < n; ++i) {
+        const o = i * 6;
+        const io = i * 12;
+        ib[io] = o; ib[io + 1] = o + 1; ib[io + 2] = o + 2;
+        ib[io + 3] = o + 1; ib[io + 4] = o + 4; ib[io + 5] = o + 2;
+        ib[io + 6] = o + 2; ib[io + 7] = o + 4; ib[io + 8] = o + 3;
+        ib[io + 9] = o + 4; ib[io + 10] = o + 5; ib[io + 11] = o + 3;
+    }
+}

+ 278 - 0
src/mol-geo/geometry/cylinders/cylinders.ts

@@ -0,0 +1,278 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from '../../../mol-util';
+import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
+import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
+import { GeometryUtils } from '../geometry';
+import { createColors } from '../color-data';
+import { createMarkers } from '../marker-data';
+import { createSizes, getMaxSize } from '../size-data';
+import { TransformData } from '../transform-data';
+import { LocationIterator, PositionLocation } from '../../util/location-iterator';
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
+import { Sphere3D } from '../../../mol-math/geometry';
+import { Theme } from '../../../mol-theme/theme';
+import { Color } from '../../../mol-util/color';
+import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
+import { createEmptyTransparency } from '../transparency-data';
+import { hashFnv32a } from '../../../mol-data/util';
+import { createEmptyClipping } from '../clipping-data';
+import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
+import { RenderableState } from '../../../mol-gl/renderable';
+
+export interface Cylinders {
+    readonly kind: 'cylinders',
+
+    /** Number of cylinders */
+    cylinderCount: number,
+
+    /** Mapping buffer as array of uvw values wrapped in a value cell */
+    readonly mappingBuffer: ValueCell<Float32Array>,
+    /** Index buffer as array of vertex index triplets wrapped in a value cell */
+    readonly indexBuffer: ValueCell<Uint32Array>,
+    /** Group buffer as array of group ids for each vertex wrapped in a value cell */
+    readonly groupBuffer: ValueCell<Float32Array>,
+    /** Cylinder start buffer as array of xyz values wrapped in a value cell */
+    readonly startBuffer: ValueCell<Float32Array>,
+    /** Cylinder end buffer as array of xyz values wrapped in a value cell */
+    readonly endBuffer: ValueCell<Float32Array>,
+    /** Cylinder scale buffer as array of scaling factors wrapped in a value cell */
+    readonly scaleBuffer: ValueCell<Float32Array>,
+    /** Cylinder cap buffer as array of cap flags wrapped in a value cell */
+    readonly capBuffer: ValueCell<Float32Array>,
+
+    /** Bounding sphere of the cylinders */
+    readonly boundingSphere: Sphere3D
+    /** Maps group ids to cylinder indices */
+    readonly groupMapping: GroupMapping
+
+    setBoundingSphere(boundingSphere: Sphere3D): void
+}
+
+export namespace Cylinders {
+    export function create(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, scales: Float32Array, caps: Float32Array, cylinderCount: number, cylinders?: Cylinders): Cylinders {
+        return cylinders ?
+            update(mappings, indices, groups, starts, ends, scales, caps, cylinderCount, cylinders) :
+            fromArrays(mappings, indices, groups, starts, ends, scales, caps, cylinderCount);
+    }
+
+    export function createEmpty(cylinders?: Cylinders): Cylinders {
+        const mb = cylinders ? cylinders.mappingBuffer.ref.value : new Float32Array(0);
+        const ib = cylinders ? cylinders.indexBuffer.ref.value : new Uint32Array(0);
+        const gb = cylinders ? cylinders.groupBuffer.ref.value : new Float32Array(0);
+        const sb = cylinders ? cylinders.startBuffer.ref.value : new Float32Array(0);
+        const eb = cylinders ? cylinders.endBuffer.ref.value : new Float32Array(0);
+        const ab = cylinders ? cylinders.scaleBuffer.ref.value : new Float32Array(0);
+        const cb = cylinders ? cylinders.capBuffer.ref.value : new Float32Array(0);
+        return create(mb, ib, gb, sb, eb, ab, cb, 0, cylinders);
+    }
+
+    function hashCode(cylinders: Cylinders) {
+        return hashFnv32a([
+            cylinders.cylinderCount, cylinders.mappingBuffer.ref.version, cylinders.indexBuffer.ref.version,
+            cylinders.groupBuffer.ref.version, cylinders.startBuffer.ref.version, cylinders.endBuffer.ref.version, cylinders.scaleBuffer.ref.version, cylinders.capBuffer.ref.version
+        ]);
+    }
+
+    function fromArrays(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, scales: Float32Array, caps: Float32Array, cylinderCount: number): Cylinders {
+
+        const boundingSphere = Sphere3D();
+        let groupMapping: GroupMapping;
+
+        let currentHash = -1;
+        let currentGroup = -1;
+
+        const cylinders = {
+            kind: 'cylinders' as const,
+            cylinderCount,
+            mappingBuffer: ValueCell.create(mappings),
+            indexBuffer: ValueCell.create(indices),
+            groupBuffer: ValueCell.create(groups),
+            startBuffer: ValueCell.create(starts),
+            endBuffer: ValueCell.create(ends),
+            scaleBuffer: ValueCell.create(scales),
+            capBuffer: ValueCell.create(caps),
+            get boundingSphere() {
+                const newHash = hashCode(cylinders);
+                if (newHash !== currentHash) {
+                    const s = calculateInvariantBoundingSphere(cylinders.startBuffer.ref.value, cylinders.cylinderCount * 6, 6);
+                    const e = calculateInvariantBoundingSphere(cylinders.endBuffer.ref.value, cylinders.cylinderCount * 6, 6);
+
+                    Sphere3D.expandBySphere(boundingSphere, s, e);
+                    currentHash = newHash;
+                }
+                return boundingSphere;
+            },
+            get groupMapping() {
+                if (cylinders.groupBuffer.ref.version !== currentGroup) {
+                    groupMapping = createGroupMapping(cylinders.groupBuffer.ref.value, cylinders.cylinderCount, 6);
+                    currentGroup = cylinders.groupBuffer.ref.version;
+                }
+                return groupMapping;
+            },
+            setBoundingSphere(sphere: Sphere3D) {
+                Sphere3D.copy(boundingSphere, sphere);
+                currentHash = hashCode(cylinders);
+            }
+        };
+        return cylinders;
+    }
+
+    function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, scales: Float32Array, caps: Float32Array, cylinderCount: number, cylinders: Cylinders) {
+        if (cylinderCount > cylinders.cylinderCount) {
+            ValueCell.update(cylinders.mappingBuffer, mappings);
+            ValueCell.update(cylinders.indexBuffer, indices);
+        }
+        cylinders.cylinderCount = cylinderCount;
+        ValueCell.update(cylinders.groupBuffer, groups);
+        ValueCell.update(cylinders.startBuffer, starts);
+        ValueCell.update(cylinders.endBuffer, ends);
+        ValueCell.update(cylinders.scaleBuffer, scales);
+        ValueCell.update(cylinders.capBuffer, caps);
+        return cylinders;
+    }
+
+    export function transform(cylinders: Cylinders, t: Mat4) {
+        const start = cylinders.startBuffer.ref.value;
+        transformPositionArray(t, start, 0, cylinders.cylinderCount * 6);
+        ValueCell.update(cylinders.startBuffer, start);
+        const end = cylinders.endBuffer.ref.value;
+        transformPositionArray(t, end, 0, cylinders.cylinderCount * 6);
+        ValueCell.update(cylinders.endBuffer, end);
+    }
+
+    //
+
+    export const Params = {
+        ...BaseGeometry.Params,
+        sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+        sizeAspectRatio: PD.Numeric(1, { min: 0, max: 3, step: 0.01 }),
+        doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
+        ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
+        xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
+    };
+    export type Params = typeof Params
+
+    export const Utils: GeometryUtils<Cylinders, Params> = {
+        Params,
+        createEmpty,
+        createValues,
+        createValuesSimple,
+        updateValues,
+        updateBoundingSphere,
+        createRenderableState,
+        updateRenderableState,
+        createPositionIterator
+    };
+
+    function createPositionIterator(cylinders: Cylinders, transform: TransformData): LocationIterator {
+        const groupCount = cylinders.cylinderCount * 6;
+        const instanceCount = transform.instanceCount.ref.value;
+        const location = PositionLocation();
+        const p = location.position;
+        const s = cylinders.startBuffer.ref.value;
+        const e = cylinders.endBuffer.ref.value;
+        const m = transform.aTransform.ref.value;
+        const getLocation = (groupIndex: number, instanceIndex: number) => {
+            const v = groupIndex % 6 === 0 ? s : e;
+            if (instanceIndex < 0) {
+                Vec3.fromArray(p, v, groupIndex * 3);
+            } else {
+                Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
+            }
+            return location;
+        };
+        return LocationIterator(groupCount, instanceCount, 2, getLocation);
+    }
+
+    function createValues(cylinders: Cylinders, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): CylindersValues {
+        const { instanceCount, groupCount } = locationIt;
+        const positionIt = createPositionIterator(cylinders, transform);
+
+        const color = createColors(locationIt, positionIt, theme.color);
+        const size = createSizes(locationIt, theme.size);
+        const marker = createMarkers(instanceCount * groupCount);
+        const overpaint = createEmptyOverpaint();
+        const transparency = createEmptyTransparency();
+        const clipping = createEmptyClipping();
+
+        const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
+
+        const padding = getMaxSize(size) * props.sizeFactor;
+        const invariantBoundingSphere = Sphere3D.clone(cylinders.boundingSphere);
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
+
+        return {
+            aMapping: cylinders.mappingBuffer,
+            aGroup: cylinders.groupBuffer,
+            aStart: cylinders.startBuffer,
+            aEnd: cylinders.endBuffer,
+            aScale: cylinders.scaleBuffer,
+            aCap: cylinders.capBuffer,
+            elements: cylinders.indexBuffer,
+            boundingSphere: ValueCell.create(boundingSphere),
+            invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
+            uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
+            ...color,
+            ...size,
+            ...marker,
+            ...overpaint,
+            ...transparency,
+            ...clipping,
+            ...transform,
+
+            padding: ValueCell.create(padding),
+
+            ...BaseGeometry.createValues(props, counts),
+            uSizeFactor: ValueCell.create(props.sizeFactor * props.sizeAspectRatio),
+            dDoubleSided: ValueCell.create(props.doubleSided),
+            dIgnoreLight: ValueCell.create(props.ignoreLight),
+            dXrayShaded: ValueCell.create(props.xrayShaded),
+        };
+    }
+
+    function createValuesSimple(cylinders: Cylinders, props: Partial<PD.Values<Params>>, colorValue: Color, sizeValue: number, transform?: TransformData) {
+        const s = BaseGeometry.createSimple(colorValue, sizeValue, transform);
+        const p = { ...PD.getDefaultValues(Params), ...props };
+        return createValues(cylinders, s.transform, s.locationIterator, s.theme, p);
+    }
+
+    function updateValues(values: CylindersValues, props: PD.Values<Params>) {
+        BaseGeometry.updateValues(values, props);
+        ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor * props.sizeAspectRatio);
+        ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
+        ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
+        ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
+    }
+
+    function updateBoundingSphere(values: CylindersValues, cylinders: Cylinders) {
+        const invariantBoundingSphere = Sphere3D.clone(cylinders.boundingSphere);
+        const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value);
+
+        if (!Sphere3D.equals(boundingSphere, values.boundingSphere.ref.value)) {
+            ValueCell.update(values.boundingSphere, boundingSphere);
+        }
+        if (!Sphere3D.equals(invariantBoundingSphere, values.invariantBoundingSphere.ref.value)) {
+            ValueCell.update(values.invariantBoundingSphere, invariantBoundingSphere);
+            ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
+        }
+    }
+
+    function createRenderableState(props: PD.Values<Params>): RenderableState {
+        const state = BaseGeometry.createRenderableState(props);
+        updateRenderableState(state, props);
+        return state;
+    }
+
+    function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
+        BaseGeometry.updateRenderableState(state, props);
+        state.opaque = state.opaque && !props.xrayShaded;
+        state.writeDepth = state.opaque;
+    }
+}

+ 18 - 11
src/mol-geo/geometry/geometry.ts

@@ -22,28 +22,31 @@ import { Theme } from '../../mol-theme/theme';
 import { RenderObjectValues } from '../../mol-gl/render-object';
 import { TextureMesh } from './texture-mesh/texture-mesh';
 import { Image } from './image/image';
+import { Cylinders } from './cylinders/cylinders';
 
-export type GeometryKind = 'mesh' | 'points' | 'spheres' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
+export type GeometryKind = 'mesh' | 'points' | 'spheres' | 'cylinders' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
 
 export type Geometry<T extends GeometryKind = GeometryKind> =
     T extends 'mesh' ? Mesh :
         T extends 'points' ? Points :
             T extends 'spheres' ? Spheres :
-                T extends 'text' ? Text :
-                    T extends 'lines' ? Lines :
-                        T extends 'direct-volume' ? DirectVolume :
-                            T extends 'image' ? Image :
-                                T extends 'texture-mesh' ? TextureMesh : never
+                T extends 'cylinders' ? Cylinders :
+                    T extends 'text' ? Text :
+                        T extends 'lines' ? Lines :
+                            T extends 'direct-volume' ? DirectVolume :
+                                T extends 'image' ? Image :
+                                    T extends 'texture-mesh' ? TextureMesh : never
 
 type GeometryParams<T extends GeometryKind> =
     T extends 'mesh' ? Mesh.Params :
         T extends 'points' ? Points.Params :
             T extends 'spheres' ? Spheres.Params :
-                T extends 'text' ? Text.Params :
-                    T extends 'lines' ? Lines.Params :
-                        T extends 'direct-volume' ? DirectVolume.Params :
-                            T extends 'image' ? Image.Params :
-                                T extends 'texture-mesh' ? TextureMesh.Params : never
+                T extends 'cylinders' ? Cylinders.Params :
+                    T extends 'text' ? Text.Params :
+                        T extends 'lines' ? Lines.Params :
+                            T extends 'direct-volume' ? DirectVolume.Params :
+                                T extends 'image' ? Image.Params :
+                                    T extends 'texture-mesh' ? TextureMesh.Params : never
 
 export interface GeometryUtils<G extends Geometry, P extends PD.Params = GeometryParams<G['kind']>, V = RenderObjectValues<G['kind']>> {
     Params: P
@@ -65,6 +68,7 @@ export namespace Geometry {
             case 'mesh': return geometry.triangleCount * 3;
             case 'points': return geometry.pointCount;
             case 'spheres': return geometry.sphereCount * 2 * 3;
+            case 'cylinders': return geometry.cylinderCount * 4 * 3;
             case 'text': return geometry.charCount * 2 * 3;
             case 'lines': return geometry.lineCount * 2 * 3;
             case 'direct-volume': return 12 * 3;
@@ -78,6 +82,7 @@ export namespace Geometry {
             case 'mesh': return geometry.vertexCount;
             case 'points': return geometry.pointCount;
             case 'spheres': return geometry.sphereCount * 4;
+            case 'cylinders': return geometry.cylinderCount * 6;
             case 'text': return geometry.charCount * 4;
             case 'lines': return geometry.lineCount * 4;
             case 'direct-volume':
@@ -93,6 +98,7 @@ export namespace Geometry {
             case 'mesh':
             case 'points':
             case 'spheres':
+            case 'cylinders':
             case 'text':
             case 'lines':
                 return getDrawCount(geometry) === 0 ? 0 : (arrayMax(geometry.groupBuffer.ref.value) + 1);
@@ -111,6 +117,7 @@ export namespace Geometry {
             case 'mesh': return Mesh.Utils as any;
             case 'points': return Points.Utils as any;
             case 'spheres': return Spheres.Utils as any;
+            case 'cylinders': return Cylinders.Utils as any;
             case 'text': return Text.Utils as any;
             case 'lines': return Lines.Utils as any;
             case 'direct-volume': return DirectVolume.Utils as any;

+ 2 - 2
src/mol-geo/geometry/lines/lines.ts

@@ -93,7 +93,7 @@ export namespace Lines {
     function hashCode(lines: Lines) {
         return hashFnv32a([
             lines.lineCount, lines.mappingBuffer.ref.version, lines.indexBuffer.ref.version,
-            lines.groupBuffer.ref.version, lines.startBuffer.ref.version, lines.startBuffer.ref.version
+            lines.groupBuffer.ref.version, lines.startBuffer.ref.version, lines.endBuffer.ref.version
         ]);
     }
 
@@ -164,7 +164,7 @@ export namespace Lines {
 
     export const Params = {
         ...BaseGeometry.Params,
-        sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+        sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
         lineSizeAttenuation: PD.Boolean(false),
     };
     export type Params = typeof Params

+ 1 - 1
src/mol-geo/geometry/points/points.ts

@@ -119,7 +119,7 @@ export namespace Points {
 
     export const Params = {
         ...BaseGeometry.Params,
-        sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
+        sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
         pointSizeAttenuation: PD.Boolean(false),
         pointFilledCircle: PD.Boolean(false),
         pointEdgeBleach: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }),

+ 9 - 6
src/mol-gl/render-object.ts

@@ -15,6 +15,7 @@ import { SpheresValues, SpheresRenderable } from './renderable/spheres';
 import { TextValues, TextRenderable } from './renderable/text';
 import { TextureMeshValues, TextureMeshRenderable } from './renderable/texture-mesh';
 import { ImageValues, ImageRenderable } from './renderable/image';
+import { CylindersRenderable, CylindersValues } from './renderable/cylinders';
 
 const getNextId = idFactory(0, 0x7FFFFFFF);
 
@@ -28,17 +29,18 @@ export interface GraphicsRenderObject<T extends RenderObjectType = RenderObjectT
     readonly materialId: number
 }
 
-export type RenderObjectType = 'mesh' | 'points' | 'spheres' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
+export type RenderObjectType = 'mesh' | 'points' | 'spheres' | 'cylinders' | 'text' | 'lines' | 'direct-volume' | 'image' | 'texture-mesh'
 
 export type RenderObjectValues<T extends RenderObjectType> =
     T extends 'mesh' ? MeshValues :
         T extends 'points' ? PointsValues :
             T extends 'spheres' ? SpheresValues :
-                T extends 'text' ? TextValues :
-                    T extends 'lines' ? LinesValues :
-                        T extends 'direct-volume' ? DirectVolumeValues :
-                            T extends 'image' ? ImageValues :
-                                T extends 'texture-mesh' ? TextureMeshValues : never
+                T extends 'cylinders' ? CylindersValues :
+                    T extends 'text' ? TextValues :
+                        T extends 'lines' ? LinesValues :
+                            T extends 'direct-volume' ? DirectVolumeValues :
+                                T extends 'image' ? ImageValues :
+                                    T extends 'texture-mesh' ? TextureMeshValues : never
 
 //
 
@@ -51,6 +53,7 @@ export function createRenderable<T extends RenderObjectType>(ctx: WebGLContext,
         case 'mesh': return MeshRenderable(ctx, o.id, o.values as MeshValues, o.state, o.materialId);
         case 'points': return PointsRenderable(ctx, o.id, o.values as PointsValues, o.state, o.materialId);
         case 'spheres': return SpheresRenderable(ctx, o.id, o.values as SpheresValues, o.state, o.materialId);
+        case 'cylinders': return CylindersRenderable(ctx, o.id, o.values as CylindersValues, o.state, o.materialId);
         case 'text': return TextRenderable(ctx, o.id, o.values as TextValues, o.state, o.materialId);
         case 'lines': return LinesRenderable(ctx, o.id, o.values as LinesValues, o.state, o.materialId);
         case 'direct-volume': return DirectVolumeRenderable(ctx, o.id, o.values as DirectVolumeValues, o.state, o.materialId);

+ 40 - 0
src/mol-gl/renderable/cylinders.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+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, GlobalTextureSchema } from './schema';
+import { CylindersShaderCode } from '../shader-code';
+import { ValueCell } from '../../mol-util';
+
+export const CylindersSchema = {
+    ...BaseSchema,
+    ...SizeSchema,
+    aStart: AttributeSpec('float32', 3, 0),
+    aEnd: AttributeSpec('float32', 3, 0),
+    aMapping: AttributeSpec('float32', 3, 0),
+    aScale: AttributeSpec('float32', 1, 0),
+    aCap: AttributeSpec('float32', 1, 0),
+    elements: ElementsSpec('uint32'),
+
+    padding: ValueSpec('number'),
+    dDoubleSided: DefineSpec('boolean'),
+    dIgnoreLight: DefineSpec('boolean'),
+    dXrayShaded: DefineSpec('boolean'),
+};
+export type CylindersSchema = typeof CylindersSchema
+export type CylindersValues = Values<CylindersSchema>
+
+export function CylindersRenderable(ctx: WebGLContext, id: number, values: CylindersValues, state: RenderableState, materialId: number): Renderable<CylindersValues> {
+    const schema = { ...GlobalUniformSchema, ...GlobalTextureSchema, ...InternalSchema, ...CylindersSchema };
+    const internalValues: InternalValues = {
+        uObjectId: ValueCell.create(id),
+    };
+    const shaderCode = CylindersShaderCode;
+    const renderItem = createGraphicsRenderItem(ctx, 'triangles', shaderCode, schema, { ...values, ...internalValues }, materialId);
+    return createRenderable(renderItem, values, state);
+}

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

@@ -135,6 +135,7 @@ export const GlobalUniformSchema = {
     uTransparentBackground: UniformSpec('b'),
 
     uClipObjectType: UniformSpec('i[]'),
+    uClipObjectInvert: UniformSpec('b[]'),
     uClipObjectPosition: UniformSpec('v3[]'),
     uClipObjectRotation: UniformSpec('v4[]'),
     uClipObjectScale: UniformSpec('v3[]'),

+ 45 - 15
src/mol-gl/renderer.ts

@@ -51,7 +51,8 @@ interface Renderer {
     renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
-    renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlendedVolumeOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlendedVolumeTransparent: (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
 
@@ -95,10 +96,11 @@ export const RendererParams = {
         variant: PD.Select('instance', PD.arrayToOptions<Clipping.Variant>(['instance', 'pixel'])),
         objects: PD.ObjectList({
             type: PD.Select('plane', PD.objectToOptions(Clipping.Type, t => stringToWords(t))),
+            invert: PD.Boolean(false),
             position: PD.Vec3(Vec3()),
             rotation: PD.Group({
                 axis: PD.Vec3(Vec3.create(1, 0, 0)),
-                angle: PD.Numeric(0, { min: -180, max: 180, step: 0.1 }, { description: 'Angle in Degrees' }),
+                angle: PD.Numeric(0, { min: -180, max: 180, step: 1 }, { description: 'Angle in Degrees' }),
             }, { isExpanded: true }),
             scale: PD.Vec3(Vec3.create(1, 1, 1)),
         }, o => stringToWords(o.type))
@@ -117,22 +119,22 @@ function getStyle(props: RendererProps['style']) {
             };
         case 'matte':
             return {
-                lightIntensity: 0.6, ambientIntensity: 0.4,
+                lightIntensity: 0.7, ambientIntensity: 0.3,
                 metalness: 0, roughness: 1, reflectivity: 0.5
             };
         case 'glossy':
             return {
-                lightIntensity: 0.6, ambientIntensity: 0.4,
+                lightIntensity: 0.7, ambientIntensity: 0.3,
                 metalness: 0, roughness: 0.4, reflectivity: 0.5
             };
         case 'metallic':
             return {
-                lightIntensity: 0.6, ambientIntensity: 0.4,
-                metalness: 0.4, roughness: 0.6, reflectivity: 0.5
+                lightIntensity: 0.7, ambientIntensity: 0.7,
+                metalness: 0.6, roughness: 0.6, reflectivity: 0.5
             };
         case 'plastic':
             return {
-                lightIntensity: 0.6, ambientIntensity: 0.4,
+                lightIntensity: 0.7, ambientIntensity: 0.3,
                 metalness: 0, roughness: 0.2, reflectivity: 0.5
             };
     }
@@ -143,6 +145,7 @@ type Clip = {
     objects: {
         count: number
         type: number[]
+        invert: boolean[]
         position: number[]
         rotation: number[]
         scale: number[]
@@ -151,8 +154,9 @@ type Clip = {
 
 const tmpQuat = Quat();
 function getClip(props: RendererProps['clip'], clip?: Clip): Clip {
-    const { type, position, rotation, scale } = clip?.objects || {
+    const { type, invert, position, rotation, scale } = clip?.objects || {
         type: (new Array(5)).fill(1),
+        invert: (new Array(5)).fill(false),
         position: (new Array(5 * 3)).fill(0),
         rotation: (new Array(5 * 4)).fill(0),
         scale: (new Array(5 * 3)).fill(1),
@@ -160,13 +164,14 @@ function getClip(props: RendererProps['clip'], clip?: Clip): Clip {
     for (let i = 0, il = props.objects.length; i < il; ++i) {
         const p = props.objects[i];
         type[i] = Clipping.Type[p.type];
+        invert[i] = p.invert;
         Vec3.toArray(p.position, position, i * 3);
         Quat.toArray(Quat.setAxisAngle(tmpQuat, p.rotation.axis, degToRad(p.rotation.angle)), rotation, i * 4);
         Vec3.toArray(p.scale, scale, i * 3);
     }
     return {
         variant: props.variant,
-        objects: { count: props.objects.length, type, position, rotation, scale }
+        objects: { count: props.objects.length, type, invert, position, rotation, scale }
     };
 }
 
@@ -231,6 +236,7 @@ namespace Renderer {
             uTransparentBackground: ValueCell.create(false),
 
             uClipObjectType: ValueCell.create(clip.objects.type),
+            uClipObjectInvert: ValueCell.create(clip.objects.invert),
             uClipObjectPosition: ValueCell.create(clip.objects.position),
             uClipObjectRotation: ValueCell.create(clip.objects.rotation),
             uClipObjectScale: ValueCell.create(clip.objects.scale),
@@ -459,7 +465,7 @@ namespace Renderer {
             }
         };
 
-        const renderBlendedVolume = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+        const renderBlendedVolumeOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
             state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
             state.enable(gl.BLEND);
 
@@ -468,7 +474,32 @@ namespace Renderer {
             const { renderables } = group;
             for (let i = 0, il = renderables.length; i < il; ++i) {
                 const r = renderables[i];
-                renderObject(r, 'colorBlended');
+
+                // TODO: simplify, handle on renderable.state???
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if (alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && !r.values.dXrayShaded?.ref.value) {
+                    renderObject(r, 'colorBlended');
+                }
+            }
+        };
+
+        const renderBlendedVolumeTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+            state.enable(gl.BLEND);
+
+            updateInternal(group, camera, depthTexture, false);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+
+                // TODO: simplify, handle on renderable.state???
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dXrayShaded?.ref.value) {
+                    renderObject(r, 'colorBlended');
+                }
             }
         };
 
@@ -537,7 +568,8 @@ namespace Renderer {
             renderBlended,
             renderBlendedOpaque,
             renderBlendedTransparent,
-            renderBlendedVolume,
+            renderBlendedVolumeOpaque,
+            renderBlendedVolumeTransparent,
             renderWboitOpaque,
             renderWboitTransparent,
 
@@ -612,9 +644,7 @@ namespace Renderer {
                 }
             },
 
-            get props() {
-                return p;
-            },
+            props: p,
             get stats(): RendererStats {
                 return {
                     programCount: ctx.stats.resourceCounts.program,

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

@@ -121,6 +121,8 @@ export function ShaderCode(name: string, vert: string, frag: string, extensions:
     return { id: shaderCodeId(), name, vert: addIncludes(vert), frag: addIncludes(frag), extensions };
 }
 
+// Note: `drawBuffers` need to be 'optional' for wboit
+
 import points_vert from './shader/points.vert';
 import points_frag from './shader/points.frag';
 export const PointsShaderCode = ShaderCode('points', points_vert, points_frag, { drawBuffers: 'optional' });
@@ -129,6 +131,10 @@ 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', drawBuffers: 'optional' });
 
+import cylinders_vert from './shader/cylinders.vert';
+import cylinders_frag from './shader/cylinders.frag';
+export const CylindersShaderCode = ShaderCode('cylinders', cylinders_vert, cylinders_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', drawBuffers: 'optional' });

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

@@ -1,6 +1,6 @@
 export default `
-float fogDepth = length(vViewPosition);
-float fogFactor = smoothstep(uFogNear, uFogFar, fogDepth);
+float viewZ = depthToViewZ(uIsOrtho, fragmentDepth, uNear, uFar);
+float fogFactor = smoothstep(uFogNear, uFogFar, abs(viewZ));
 float fogAlpha = (1.0 - fogFactor) * gl_FragColor.a;
 float preFogAlpha = gl_FragColor.a;
 if (!uTransparentBackground) {

+ 2 - 2
src/mol-gl/shader/chunks/check-picking-alpha.glsl.ts

@@ -1,6 +1,6 @@
 export default `
-float depth = length(vViewPosition);
-float fogFactor = smoothstep(uFogNear, uFogFar, depth);
+float viewZ = depthToViewZ(uIsOrtho, fragmentDepth, uNear, uFar);
+float fogFactor = smoothstep(uFogNear, uFogFar, abs(viewZ));
 float alpha = (1.0 - fogFactor) * uAlpha;
 if (uAlpha < uPickingAlphaThreshold || alpha < 0.1)
     discard; // ignore so the element below can be picked

+ 1 - 1
src/mol-gl/shader/chunks/color-frag-params.glsl.ts

@@ -10,7 +10,7 @@ export default `
         varying vec4 vOverpaint;
     #endif
 #elif defined(dRenderVariant_pick)
-    #if __VERSION__ != 300
+    #if __VERSION__ == 100
         varying vec4 vColor;
     #else
         flat in vec4 vColor;

+ 2 - 2
src/mol-gl/shader/chunks/color-vert-params.glsl.ts

@@ -12,7 +12,7 @@ export default `
     #endif
 
     #if defined(dColorType_vertex) || defined(dColorType_vertexInstance)
-        #if __VERSION__ != 300
+        #if __VERSION__ == 100
             attribute float aVertex;
         #else
             #define aVertex float(gl_VertexID)
@@ -25,7 +25,7 @@ export default `
         uniform sampler2D tOverpaint;
     #endif
 #elif defined(dRenderVariant_pick)
-    #if __VERSION__ != 300
+    #if __VERSION__ == 100
         varying vec4 vColor;
     #else
         flat out vec4 vColor;

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

@@ -63,7 +63,7 @@ export default `
         }
     }
 
-    #if __VERSION__ != 300
+    #if __VERSION__ == 100
         // 8-bit
         int bitwiseAnd(int a, int b) {
             int d = 128;
@@ -92,8 +92,10 @@ export default `
         for (int i = 0; i < dClipObjectCount; ++i) {
             if (flag == 0 || hasBit(flag, i + 1)) {
                 // TODO take sphere radius into account?
-                if (getSignedDistance(sphere.xyz, uClipObjectType[i], uClipObjectPosition[i], uClipObjectRotation[i], uClipObjectScale[i]) <= 0.0)
+                bool test = getSignedDistance(sphere.xyz, uClipObjectType[i], uClipObjectPosition[i], uClipObjectRotation[i], uClipObjectScale[i]) <= 0.0;
+                if ((!uClipObjectInvert[i] && test) || (uClipObjectInvert[i] && !test)) {
                     return true;
+                }
             }
         }
         return false;

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

@@ -5,12 +5,13 @@ uniform int uGroupCount;
 
 #if dClipObjectCount != 0
     uniform int uClipObjectType[dClipObjectCount];
+    uniform bool uClipObjectInvert[dClipObjectCount];
     uniform vec3 uClipObjectPosition[dClipObjectCount];
     uniform vec4 uClipObjectRotation[dClipObjectCount];
     uniform vec3 uClipObjectScale[dClipObjectCount];
 
     #if defined(dClipping)
-        #if __VERSION__ != 300
+        #if __VERSION__ == 100
             varying float vClipping;
         #else
             flat in float vClipping;
@@ -20,7 +21,7 @@ uniform int uGroupCount;
 
 uniform vec3 uHighlightColor;
 uniform vec3 uSelectColor;
-#if __VERSION__ != 300
+#if __VERSION__ == 100
     varying float vMarker;
 #else
     flat in float vMarker;
@@ -31,6 +32,10 @@ varying vec3 vViewPosition;
 
 uniform vec2 uViewOffset;
 
+uniform float uNear;
+uniform float uFar;
+uniform float uIsOrtho;
+
 uniform float uFogNear;
 uniform float uFogFar;
 uniform vec3 uFogColor;

+ 3 - 2
src/mol-gl/shader/chunks/common-vert-params.glsl.ts

@@ -10,6 +10,7 @@ uniform vec4 uInvariantBoundingSphere;
 
 #if dClipObjectCount != 0
     uniform int uClipObjectType[dClipObjectCount];
+    uniform bool uClipObjectInvert[dClipObjectCount];
     uniform vec3 uClipObjectPosition[dClipObjectCount];
     uniform vec4 uClipObjectRotation[dClipObjectCount];
     uniform vec3 uClipObjectScale[dClipObjectCount];
@@ -17,7 +18,7 @@ uniform vec4 uInvariantBoundingSphere;
     #if defined(dClipping)
         uniform vec2 uClippingTexDim;
         uniform sampler2D tClipping;
-        #if __VERSION__ != 300
+        #if __VERSION__ == 100
             varying float vClipping;
         #else
             flat out float vClipping;
@@ -27,7 +28,7 @@ uniform vec4 uInvariantBoundingSphere;
 
 uniform vec2 uMarkerTexDim;
 uniform sampler2D tMarker;
-#if __VERSION__ != 300
+#if __VERSION__ == 100
     varying float vMarker;
 #else
     flat out float vMarker;

+ 34 - 3
src/mol-gl/shader/chunks/common.glsl.ts

@@ -29,7 +29,7 @@ float intDiv(const in float a, const in float b) { return float(int(a) / int(b))
 vec2 ivec2Div(const in vec2 a, const in vec2 b) { return vec2(ivec2(a) / ivec2(b)); }
 float intMod(const in float a, const in float b) { return a - b * float(int(a) / int(b)); }
 
-float pow2(const in float x) { return x*x; }
+float pow2(const in float x) { return x * x; }
 
 const float maxFloat = 10000.0; // NOTE constant also set in TypeScript
 const float floatLogFactor = 9.210440366976517; // log(maxFloat + 1.0);
@@ -50,6 +50,25 @@ float decodeFloatRGB(const in vec3 rgb) {
     return (rgb.r * 256.0 * 256.0 * 255.0 + rgb.g * 256.0 * 255.0 + rgb.b * 255.0) - 1.0;
 }
 
+vec2 packUnitIntervalToRG(const in float v) {
+    vec2 enc;
+    enc.xy = vec2(fract(v * 256.0), v);
+    enc.y -= enc.x * (1.0 / 256.0);
+    enc.xy *=  256.0 / 255.0;
+
+    return enc;
+}
+
+float unpackRGToUnitInterval(const in vec2 enc) {
+    return dot(enc, vec2(255.0 / (256.0 * 256.0), 255.0 / 256.0));
+}
+
+vec3 screenSpaceToViewSpace(const in vec3 ssPos, const in mat4 invProjection) {
+    vec4 p = vec4(ssPos * 2.0 - 1.0, 1.0);
+    p = invProjection * p;
+    return p.xyz / p.w;
+}
+
 const float PackUpscale = 256.0 / 255.0; // fraction -> 0..1 (including 1)
 const float UnpackDownscale = 255.0 / 256.0; // 0..1 -> fraction (excluding 1)
 const vec3 PackFactors = vec3(256.0 * 256.0 * 256.0, 256.0 * 256.0,  256.0);
@@ -72,11 +91,23 @@ vec4 linearTosRGB(const in vec4 c) {
     return vec4(mix(pow(c.rgb, vec3(0.41666)) * 1.055 - vec3(0.055), c.rgb * 12.92, vec3(lessThanEqual(c.rgb, vec3(0.0031308)))), c.a);
 }
 
-float linearizeDepth(in float depth, in float near, in float far) {
+float linearizeDepth(const in float depth, const in float near, const in float far) {
     return (2.0 * near) / (far + near - depth * (far - near));
 }
 
-#if __VERSION__ != 300
+float perspectiveDepthToViewZ(const in float invClipZ, const in float near, const in float far) {
+    return (near * far) / ((far - near) * invClipZ - far);
+}
+
+float orthographicDepthToViewZ(const in float linearClipZ, const in float near, const in float far) {
+    return linearClipZ * (near - far) - near;
+}
+
+float depthToViewZ(const in float isOrtho, const in float linearClipZ, const in float near, const in float far) {
+    return isOrtho == 1.0 ? orthographicDepthToViewZ(linearClipZ, near, far) : perspectiveDepthToViewZ(linearClipZ, near, far);
+}
+
+#if __VERSION__ == 100
     // transpose
 
     float transpose(const in float m) {

+ 50 - 50
src/mol-gl/shader/chunks/light-frag-params.glsl.ts

@@ -15,97 +15,97 @@ uniform float uMetalness;
 uniform float uRoughness;
 
 struct PhysicalMaterial {
-	vec3 diffuseColor;
-	float specularRoughness;
-	vec3 specularColor;
+    vec3 diffuseColor;
+    float specularRoughness;
+    vec3 specularColor;
 };
 
 struct IncidentLight {
-	vec3 color;
-	vec3 direction;
+    vec3 color;
+    vec3 direction;
 };
 
 struct ReflectedLight {
-	vec3 directDiffuse;
-	vec3 directSpecular;
-	vec3 indirectDiffuse;
+    vec3 directDiffuse;
+    vec3 directSpecular;
+    vec3 indirectDiffuse;
 };
 
 struct GeometricContext {
-	vec3 position;
-	vec3 normal;
-	vec3 viewDir;
+    vec3 position;
+    vec3 normal;
+    vec3 viewDir;
 };
 
 vec3 F_Schlick(const in vec3 specularColor, const in float dotLH) {
-	// Original approximation by Christophe Schlick '94
-	// float fresnel = pow( 1.0 - dotLH, 5.0 );
-	// Optimized variant (presented by Epic at SIGGRAPH '13)
-	// https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
-	float fresnel = exp2((-5.55473 * dotLH - 6.98316) * dotLH);
-	return (1.0 - specularColor) * fresnel + specularColor;
+    // Original approximation by Christophe Schlick '94
+    // float fresnel = pow( 1.0 - dotLH, 5.0 );
+    // Optimized variant (presented by Epic at SIGGRAPH '13)
+    // https://cdn2.unrealengine.com/Resources/files/2013SiggraphPresentationsNotes-26915738.pdf
+    float fresnel = exp2((-5.55473 * dotLH - 6.98316) * dotLH);
+    return (1.0 - specularColor) * fresnel + specularColor;
 }
 
 // Moving Frostbite to Physically Based Rendering 3.0 - page 12, listing 2
 // https://seblagarde.files.wordpress.com/2015/07/course_notes_moving_frostbite_to_pbr_v32.pdf
 float G_GGX_SmithCorrelated(const in float alpha, const in float dotNL, const in float dotNV) {
-	float a2 = pow2(alpha);
-	// dotNL and dotNV are explicitly swapped. This is not a mistake.
-	float gv = dotNL * sqrt(a2 + (1.0 - a2) * pow2(dotNV));
-	float gl = dotNV * sqrt(a2 + (1.0 - a2) * pow2(dotNL));
-	return 0.5 / max(gv + gl, EPSILON);
+    float a2 = pow2(alpha);
+    // dotNL and dotNV are explicitly swapped. This is not a mistake.
+    float gv = dotNL * sqrt(a2 + (1.0 - a2) * pow2(dotNV));
+    float gl = dotNV * sqrt(a2 + (1.0 - a2) * pow2(dotNL));
+    return 0.5 / max(gv + gl, EPSILON);
 }
 
 // Microfacet Models for Refraction through Rough Surfaces - equation (33)
 // http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
 // alpha is "roughness squared" in Disney’s reparameterization
 float D_GGX(const in float alpha, const in float dotNH) {
-	float a2 = pow2(alpha);
-	float denom = pow2(dotNH) * (a2 - 1.0) + 1.0; // avoid alpha = 0 with dotNH = 1
-	return RECIPROCAL_PI * a2 / pow2(denom);
+    float a2 = pow2(alpha);
+    float denom = pow2(dotNH) * (a2 - 1.0) + 1.0; // avoid alpha = 0 with dotNH = 1
+    return RECIPROCAL_PI * a2 / pow2(denom);
 }
 
 vec3 BRDF_Diffuse_Lambert(const in vec3 diffuseColor) {
-	return RECIPROCAL_PI * diffuseColor;
+    return RECIPROCAL_PI * diffuseColor;
 }
 
 // GGX Distribution, Schlick Fresnel, GGX-Smith Visibility
 vec3 BRDF_Specular_GGX(const in IncidentLight incidentLight, const in GeometricContext geometry, const in vec3 specularColor, const in float roughness) {
-	float alpha = pow2(roughness); // UE4's roughness
-	vec3 halfDir = normalize(incidentLight.direction + geometry.viewDir);
-
-	float dotNL = saturate(dot(geometry.normal, incidentLight.direction));
-	float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
-	float dotNH = saturate(dot(geometry.normal, halfDir));
-	float dotLH = saturate(dot(incidentLight.direction, halfDir));
-
-	vec3 F = F_Schlick(specularColor, dotLH);
-	float G = G_GGX_SmithCorrelated(alpha, dotNL, dotNV);
-	float D = D_GGX(alpha, dotNH);
-	return F * (G * D);
+    float alpha = pow2(roughness); // UE4's roughness
+    vec3 halfDir = normalize(incidentLight.direction + geometry.viewDir);
+
+    float dotNL = saturate(dot(geometry.normal, incidentLight.direction));
+    float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
+    float dotNH = saturate(dot(geometry.normal, halfDir));
+    float dotLH = saturate(dot(incidentLight.direction, halfDir));
+
+    vec3 F = F_Schlick(specularColor, dotLH);
+    float G = G_GGX_SmithCorrelated(alpha, dotNL, dotNV);
+    float D = D_GGX(alpha, dotNH);
+    return F * (G * D);
 }
 
 // ref: https://www.unrealengine.com/blog/physically-based-shading-on-mobile - environmentBRDF for GGX on mobile
 vec3 BRDF_Specular_GGX_Environment(const in GeometricContext geometry, const in vec3 specularColor, const in float roughness) {
-	float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
-	const vec4 c0 = vec4(-1, -0.0275, -0.572, 0.022);
-	const vec4 c1 = vec4(1, 0.0425, 1.04, -0.04);
-	vec4 r = roughness * c0 + c1;
-	float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;
-	vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
-	return specularColor * AB.x + AB.y;
+    float dotNV = saturate(dot(geometry.normal, geometry.viewDir));
+    const vec4 c0 = vec4(-1, -0.0275, -0.572, 0.022);
+    const vec4 c1 = vec4(1, 0.0425, 1.04, -0.04);
+    vec4 r = roughness * c0 + c1;
+    float a004 = min(r.x * r.x, exp2(-9.28 * dotNV)) * r.x + r.y;
+    vec2 AB = vec2(-1.04, 1.04) * a004 + r.zw;
+    return specularColor * AB.x + AB.y;
 }
 
 void RE_Direct_Physical(const in IncidentLight directLight, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {
-	float dotNL = saturate(dot(geometry.normal, directLight.direction));
+    float dotNL = saturate(dot(geometry.normal, directLight.direction));
     vec3 irradiance = dotNL * directLight.color;
-	irradiance *= PI; // punctual light
+    irradiance *= PI; // punctual light
 
-	reflectedLight.directSpecular += irradiance * BRDF_Specular_GGX(directLight, geometry, material.specularColor, material.specularRoughness);
-	reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
+    reflectedLight.directSpecular += irradiance * BRDF_Specular_GGX(directLight, geometry, material.specularColor, material.specularRoughness);
+    reflectedLight.directDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
 }
 
 void RE_IndirectDiffuse_Physical(const in vec3 irradiance, const in GeometricContext geometry, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) {
-	reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
+    reflectedLight.indirectDiffuse += irradiance * BRDF_Diffuse_Lambert(material.diffuseColor);
 }
 `;

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

@@ -1,3 +1,10 @@
+/**
+ * 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>
+ */
+
 export default `
 #if defined(dRenderVariant_colorWboit)
     if (!uRenderWboit) {

+ 12 - 0
src/mol-gl/shader/copy.frag.ts

@@ -0,0 +1,12 @@
+export const copy_frag = `
+precision highp float;
+precision highp sampler2D;
+
+uniform sampler2D tColor;
+uniform vec2 uTexSize;
+
+void main() {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    gl_FragColor = texture2D(tColor, coords);
+}
+`;

+ 139 - 0
src/mol-gl/shader/cylinders.frag.ts

@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+
+uniform mat4 uView;
+
+varying mat4 vTransform;
+varying vec3 vStart;
+varying vec3 vEnd;
+varying float vSize;
+varying float vCap;
+
+uniform vec3 uCameraDir;
+uniform vec3 uCameraPosition;
+
+#include common
+#include common_frag_params
+#include color_frag_params
+#include light_frag_params
+#include common_clip
+#include wboit_params
+
+// adapted from https://www.shadertoy.com/view/4lcSRn
+// The MIT License, Copyright 2016 Inigo Quilez
+bool CylinderImpostor(
+    in vec3 rayOrigin, in vec3 rayDir,
+    in vec3 start, in vec3 end, in float radius,
+    out vec4 intersection, out bool interior
+){
+    vec3 ba = end - start;
+    vec3 oc = rayOrigin - start;
+
+    float baba = dot(ba, ba);
+    float bard = dot(ba, rayDir);
+    float baoc = dot(ba, oc);
+
+    float k2 = baba - bard*bard;
+    float k1 = baba * dot(oc, rayDir) - baoc * bard;
+    float k0 = baba * dot(oc, oc) - baoc * baoc - radius * radius * baba;
+
+    float h = k1 * k1 - k2 * k0;
+    if (h < 0.0) return false;
+
+    bool topCap = (vCap > 0.9 && vCap < 1.1) || vCap >= 2.9;
+    bool bottomCap = (vCap > 1.9 && vCap < 2.1) || vCap >= 2.9;
+
+    // body outside
+    h = sqrt(h);
+    float t = (-k1 - h) / k2;
+    float y = baoc + t * bard;
+    if (y > 0.0 && y < baba) {
+        interior = false;
+        intersection = vec4(t, (oc + t * rayDir - ba * y / baba) / radius);
+        return true;
+    }
+
+    if (topCap && y < 0.0) {
+        // top cap
+        t = -baoc / bard;
+        if (abs(k1 + k2 * t) < h) {
+            interior = false;
+            intersection = vec4(t, ba * sign(y) / baba);
+            return true;
+        }
+    } else if(bottomCap && y >= 0.0) {
+        // bottom cap
+        t = (baba - baoc) / bard;
+        if (abs(k1 + k2 * t) < h) {
+            interior = false;
+            intersection = vec4(t, ba * sign(y) / baba);
+            return true;
+        }
+    }
+
+    #ifdef dDoubleSided
+        // body inside
+        h = -h;
+        t = (-k1 - h) / k2;
+        y = baoc + t * bard;
+        if (y > 0.0 && y < baba) {
+            interior = true;
+            intersection = vec4(t, (oc + t * rayDir - ba * y / baba) / radius);
+            return true;
+        }
+
+        // TODO: handle inside caps???
+    #endif
+
+    return false;
+}
+
+void main() {
+    #include clip_pixel
+
+    vec3 rayDir = mix(normalize(vModelPosition - uCameraPosition), uCameraDir, uIsOrtho);
+
+    vec4 intersection;
+    bool interior;
+    bool hit = CylinderImpostor(vModelPosition, rayDir, vStart, vEnd, vSize, intersection, interior);
+    if (!hit) discard;
+
+    vec3 vViewPosition = vModelPosition + intersection.x * rayDir;
+    vViewPosition = (uView * vec4(vViewPosition, 1.0)).xyz;
+    gl_FragDepthEXT = calcDepth(vViewPosition);
+
+    // bugfix (mac only?)
+    if (gl_FragDepthEXT < 0.0) discard;
+    if (gl_FragDepthEXT > 1.0) discard;
+
+    float fragmentDepth = gl_FragDepthEXT;
+    #include assign_material_color
+
+    #if defined(dRenderVariant_pick)
+        #include check_picking_alpha
+        gl_FragColor = material;
+    #elif defined(dRenderVariant_depth)
+        gl_FragColor = material;
+    #elif defined(dRenderVariant_color)
+        #ifdef dIgnoreLight
+            gl_FragColor = material;
+        #else
+            mat3 normalMatrix = transpose3(inverse3(mat3(uView)));
+            vec3 normal = normalize(normalMatrix * -normalize(intersection.yzw));
+            #include apply_light_color
+        #endif
+
+        #include apply_interior_color
+        #include apply_marker_color
+        #include apply_fog
+        #include wboit_write
+    #endif
+}
+`;

+ 74 - 0
src/mol-gl/shader/cylinders.vert.ts

@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+
+#include common
+#include read_from_texture
+#include common_vert_params
+#include color_vert_params
+#include size_vert_params
+#include common_clip
+
+uniform mat4 uModelView;
+
+attribute mat4 aTransform;
+attribute float aInstance;
+attribute float aGroup;
+
+attribute vec3 aMapping;
+attribute vec3 aStart;
+attribute vec3 aEnd;
+attribute float aScale;
+attribute float aCap;
+
+varying mat4 vTransform;
+varying vec3 vStart;
+varying vec3 vEnd;
+varying float vSize;
+varying float vCap;
+
+uniform float uIsOrtho;
+uniform vec3 uCameraDir;
+
+void main() {
+    #include assign_group
+    #include assign_color_varying
+    #include assign_marker_varying
+    #include assign_clipping_varying
+    #include assign_size
+
+    mat4 modelTransform = uModel * aTransform;
+
+    vTransform = aTransform;
+    vStart = (modelTransform * vec4(aStart, 1.0)).xyz;
+    vEnd = (modelTransform * vec4(aEnd, 1.0)).xyz;
+    vSize = size * aScale;
+    vCap = aCap;
+
+    vModelPosition = (vStart + vEnd) * 0.5;
+    vec3 camDir = -mix(normalize(vModelPosition - uCameraPosition), uCameraDir, uIsOrtho);
+    vec3 dir = vEnd - vStart;
+    // ensure cylinder 'dir' is pointing towards the camera
+    if(dot(camDir, dir) < 0.0) dir = -dir;
+
+    vec3 left = cross(camDir, dir);
+    vec3 up = cross(left, dir);
+    left = vSize * normalize(left);
+    up = vSize * normalize(up);
+
+    // move vertex in object-space from center to corner
+    vModelPosition += aMapping.x * dir + aMapping.y * left + aMapping.z * up;
+
+    vec4 mvPosition = uView * vec4(vModelPosition, 1.0);
+    vViewPosition = mvPosition.xyz;
+    gl_Position = uProjection * mvPosition;
+
+    #include clip_instance
+}
+`;

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

@@ -14,6 +14,7 @@ precision highp int;
 
 #if dClipObjectCount != 0
     uniform int uClipObjectType[dClipObjectCount];
+    uniform bool uClipObjectInvert[dClipObjectCount];
     uniform vec3 uClipObjectPosition[dClipObjectCount];
     uniform vec4 uClipObjectRotation[dClipObjectCount];
     uniform vec3 uClipObjectScale[dClipObjectCount];
@@ -30,8 +31,6 @@ uniform vec3 uCameraDir;
 
 uniform sampler2D tDepth;
 uniform vec2 uDrawingBufferSize;
-uniform float uNear;
-uniform float uFar;
 
 varying vec3 vOrigPos;
 varying float vInstance;
@@ -70,13 +69,15 @@ uniform bool uInteriorColorFlag;
 uniform vec3 uInteriorColor;
 bool interior;
 
+uniform float uNear;
+uniform float uFar;
 uniform float uIsOrtho;
 
 uniform vec3 uCellDim;
 uniform vec3 uCameraPosition;
 uniform mat4 uCartnToUnit;
 
-#if __VERSION__ == 300
+#if __VERSION__ != 100
     // for webgl1 this is given as a 'define'
     uniform int uMaxSteps;
 #endif
@@ -166,6 +167,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
     vec4 src = vec4(0.0);
     vec4 dst = vec4(0.0);
     bool hit = false;
+    float fragmentDepth;
 
     vec3 posMin = vec3(0.0);
     vec3 posMax = vec3(1.0) - vec3(1.0) / uGridDim;
@@ -325,6 +327,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
                     #include apply_marker_color
 
                     preFogAlphaBlended = (1.0 - preFogAlphaBlended) * gl_FragColor.a + preFogAlphaBlended;
+                    fragmentDepth = depth;
                     #include apply_fog
 
                     src = gl_FragColor;
@@ -393,6 +396,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
                 #include apply_marker_color
 
                 preFogAlphaBlended = (1.0 - preFogAlphaBlended) * gl_FragColor.a + preFogAlphaBlended;
+                fragmentDepth = calcDepth(mvPosition.xyz);
                 #include apply_fog
 
                 src = gl_FragColor;
@@ -424,7 +428,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
 // TODO: support float texture for higher precision values???
 // TODO: support clipping exclusion texture support
 
-void main () {
+void main() {
     if (gl_FrontFacing)
         discard;
 

+ 7 - 3
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 common_clip
 #include wboit_params
 
 uniform vec2 uImageTexDim;
@@ -88,6 +89,8 @@ varying float vInstance;
 #endif
 
 void main() {
+    #include clip_pixel
+
     #if defined(dInterpolation_cubic)
         vec4 imageData = biCubic(tImageTex, vUv);
     #else
@@ -96,6 +99,9 @@ void main() {
     imageData.a = clamp(imageData.a, 0.0, 1.0);
     if (imageData.a > 0.9) imageData.a = 1.0;
 
+    float fragmentDepth = gl_FragCoord.z;
+    bool interior = false;
+
     #if defined(dRenderVariant_pick)
         if (imageData.a < 0.3)
             discard;
@@ -121,11 +127,9 @@ void main() {
 
         float group = decodeFloatRGB(texture2D(tGroupTex, vUv).rgb);
         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
 }

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

@@ -16,6 +16,9 @@ precision highp int;
 
 void main(){
     #include clip_pixel
+
+    bool interior = false;
+    float fragmentDepth = gl_FragCoord.z;
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)
@@ -28,9 +31,6 @@ void main(){
 
         #include apply_marker_color
         #include apply_fog
-
-        float fragmentDepth = gl_FragCoord.z;
-        bool interior = false;
         #include wboit_write
     #endif
 }

+ 0 - 1
src/mol-gl/shader/lines.vert.ts

@@ -20,7 +20,6 @@ precision highp int;
 uniform float uPixelRatio;
 uniform float uViewportHeight;
 
-attribute vec3 aPosition;
 attribute mat4 aTransform;
 attribute float aInstance;
 attribute float aGroup;

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

@@ -35,6 +35,7 @@ void main() {
         interior = !frontFacing;
     #endif
 
+    float fragmentDepth = gl_FragCoord.z;
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)
@@ -60,8 +61,6 @@ void main() {
         #include apply_interior_color
         #include apply_marker_color
         #include apply_fog
-
-        float fragmentDepth = gl_FragCoord.z;
         #include wboit_write
     #endif
 }

+ 65 - 0
src/mol-gl/shader/outlines.frag.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+uniform sampler2D tDepth;
+uniform vec2 uTexSize;
+
+uniform float uNear;
+uniform float uFar;
+
+uniform float uMaxPossibleViewZDiff;
+
+#include common
+
+float getViewZ(const in float depth) {
+    #if dOrthographic == 1
+        return orthographicDepthToViewZ(depth, uNear, uFar);
+    #else
+        return perspectiveDepthToViewZ(depth, uNear, uFar);
+    #endif
+}
+
+float getDepth(const in vec2 coords) {
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
+}
+
+bool isBackground(const in float depth) {
+    return depth == 1.0;
+}
+
+void main(void) {
+    float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
+
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    vec2 invTexSize = 1.0 / uTexSize;
+
+    float selfDepth = getDepth(coords);
+    float selfViewZ = isBackground(selfDepth) ? backgroundViewZ : getViewZ(getDepth(coords));
+
+    float outline = 1.0;
+    float bestDepth = 1.0;
+
+    for (int y = -1; y <= 1; y++) {
+        for (int x = -1; x <= 1; x++) {
+            vec2 sampleCoords = coords + vec2(float(x), float(y)) * invTexSize;
+            float sampleDepth = getDepth(sampleCoords);
+            float sampleViewZ = isBackground(sampleDepth) ? backgroundViewZ : getViewZ(sampleDepth);
+
+            if (abs(selfViewZ - sampleViewZ) > uMaxPossibleViewZDiff && selfDepth > sampleDepth && sampleDepth <= bestDepth) {
+                outline = 0.0;
+                bestDepth = sampleDepth;
+            }
+        }
+    }
+
+    gl_FragColor = vec4(outline, packUnitIntervalToRG(bestDepth), 0.0);
+}
+`;

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

@@ -23,6 +23,9 @@ const float radius = 0.5;
 
 void main(){
     #include clip_pixel
+
+    float fragmentDepth = gl_FragCoord.z;
+    bool interior = false;
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)
@@ -42,9 +45,6 @@ void main(){
 
         #include apply_marker_color
         #include apply_fog
-
-        float fragmentDepth = gl_FragCoord.z;
-        bool interior = false;
         #include wboit_write
     #endif
 }

+ 80 - 67
src/mol-gl/shader/postprocessing.frag.ts

@@ -1,10 +1,19 @@
+/**
+ * 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>
+ */
+
 export default `
 precision highp float;
 precision highp int;
 precision highp sampler2D;
 
+uniform sampler2D tSsaoDepth;
 uniform sampler2D tColor;
-uniform sampler2D tPackedDepth;
+uniform sampler2D tDepth;
+uniform sampler2D tOutlines;
 uniform vec2 uTexSize;
 
 uniform float uNear;
@@ -12,6 +21,7 @@ uniform float uFar;
 uniform float uFogNear;
 uniform float uFogFar;
 uniform vec3 uFogColor;
+uniform bool uTransparentBackground;
 
 uniform float uOcclusionBias;
 uniform float uOcclusionRadius;
@@ -19,28 +29,11 @@ uniform float uOcclusionRadius;
 uniform float uOutlineScale;
 uniform float uOutlineThreshold;
 
-const float noiseAmount = 0.0002;
-const vec4 occlusionColor = vec4(0.0, 0.0, 0.0, 1.0);
-
-#include common
-
-float noise(const in vec2 coords) {
-    float a = 12.9898;
-    float b = 78.233;
-    float c = 43758.5453;
-    float dt = dot(coords, vec2(a,b));
-    float sn = mod(dt, 3.14159);
+uniform float uMaxPossibleViewZDiff;
 
-    return fract(sin(sn) * c);
-}
-
-float perspectiveDepthToViewZ(const in float invClipZ, const in float near, const in float far) {
-    return (near * far) / ((far - near) * invClipZ - far);
-}
+const vec3 occlusionColor = vec3(0.0);
 
-float orthographicDepthToViewZ(const in float linearClipZ, const in float near, const in float far) {
-    return linearClipZ * (near - far) - near;
-}
+#include common
 
 float getViewZ(const in float depth) {
     #if dOrthographic == 1
@@ -51,68 +44,88 @@ float getViewZ(const in float depth) {
 }
 
 float getDepth(const in vec2 coords) {
-    return unpackRGBAToDepth(texture2D(tPackedDepth, coords));
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
+}
+
+bool isBackground(const in float depth) {
+    return depth == 1.0;
 }
 
-float calcSSAO(const in vec2 coords, const in float depth) {
-    float occlusionFactor = 0.0;
+float getOutline(const in vec2 coords, out float closestTexel) {
+    float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
+    vec2 invTexSize = 1.0 / uTexSize;
+
+    float selfDepth = getDepth(coords);
+    float selfViewZ = isBackground(selfDepth) ? backgroundViewZ : getViewZ(selfDepth);
 
-    for (int i = -dOcclusionKernelSize; i <= dOcclusionKernelSize; i++) {
-        for (int j = -dOcclusionKernelSize; j <= dOcclusionKernelSize; j++) {
-            vec2 coordsDelta = coords + uOcclusionRadius / float(dOcclusionKernelSize) * vec2(float(i) / uTexSize.x, float(j) / uTexSize.y);
-            coordsDelta += noiseAmount * (noise(coordsDelta) - 0.5) / uTexSize;
-            coordsDelta = clamp(coordsDelta, 0.5 / uTexSize, 1.0 - 1.0 / uTexSize);
-            if (getDepth(coordsDelta) < depth) occlusionFactor += 1.0;
+    float outline = 1.0;
+    closestTexel = 1.0;
+    for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
+        for (int x = -dOutlineScale; x <= dOutlineScale; x++) {
+            if (x * x + y * y > dOutlineScale * dOutlineScale) {
+                continue;
+            }
+
+            vec2 sampleCoords = coords + vec2(float(x), float(y)) * invTexSize;
+
+            vec4 sampleOutlineCombined = texture2D(tOutlines, sampleCoords);
+            float sampleOutline = sampleOutlineCombined.r;
+            float sampleOutlineDepth = unpackRGToUnitInterval(sampleOutlineCombined.gb);
+
+            if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel && abs(selfViewZ - sampleOutlineDepth) > uMaxPossibleViewZDiff) {
+                outline = 0.0;
+                closestTexel = sampleOutlineDepth;
+            }
         }
     }
-
-    return occlusionFactor / float((2 * dOcclusionKernelSize + 1) * (2 * dOcclusionKernelSize + 1));
+    return outline;
 }
 
-vec2 calcEdgeDepth(const in vec2 coords) {
-    vec2 invTexSize = 1.0 / uTexSize;
-    float halfScaleFloor = floor(uOutlineScale * 0.5);
-    float halfScaleCeil = ceil(uOutlineScale * 0.5);
-
-    vec2 bottomLeftUV = coords - invTexSize * halfScaleFloor;
-    vec2 topRightUV = coords + invTexSize * halfScaleCeil;
-    vec2 bottomRightUV = coords + vec2(invTexSize.x * halfScaleCeil, -invTexSize.y * halfScaleFloor);
-    vec2 topLeftUV = coords + vec2(-invTexSize.x * halfScaleFloor, invTexSize.y * halfScaleCeil);
-
-    float depth0 = getDepth(bottomLeftUV);
-    float depth1 = getDepth(topRightUV);
-    float depth2 = getDepth(bottomRightUV);
-    float depth3 = getDepth(topLeftUV);
-
-    float depthFiniteDifference0 = depth1 - depth0;
-    float depthFiniteDifference1 = depth3 - depth2;
-
-    return vec2(
-        sqrt(pow(depthFiniteDifference0, 2.0) + pow(depthFiniteDifference1, 2.0)) * 100.0,
-        min(depth0, min(depth1, min(depth2, depth3)))
-    );
+float getSsao(vec2 coords) {
+    float rawSsao = unpackRGToUnitInterval(texture2D(tSsaoDepth, coords).xy);
+    if (rawSsao > 0.999) {
+        return 1.0;
+    } else if (rawSsao > 0.001) {
+        return rawSsao;
+    }
+    return 0.0;
 }
 
 void main(void) {
     vec2 coords = gl_FragCoord.xy / uTexSize;
     vec4 color = texture2D(tColor, coords);
 
-    #ifdef dOutlineEnable
-        vec2 edgeDepth = calcEdgeDepth(coords);
-        float edgeFlag = step(edgeDepth.x, uOutlineThreshold);
-        color.rgb *= edgeFlag;
+    float viewDist;
+    float fogFactor;
 
-        float viewDist = abs(getViewZ(edgeDepth.y));
-        float fogFactor = smoothstep(uFogNear, uFogFar, viewDist) * (1.0 - edgeFlag);
-        color.rgb = mix(color.rgb, uFogColor, fogFactor);
-    #endif
-
-    // occlusion needs to be handled after outline to darken them properly
     #ifdef dOcclusionEnable
         float depth = getDepth(coords);
-        if (depth <= 0.99) {
-            float occlusionFactor = calcSSAO(coords, depth);
-            color = mix(color, occlusionColor, uOcclusionBias * occlusionFactor);
+        if (!isBackground(depth)) {
+            viewDist = abs(getViewZ(depth));
+            fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
+            float occlusionFactor = getSsao(coords);
+            if (!uTransparentBackground) {
+                color.rgb = mix(mix(occlusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
+            } else {
+                color.rgb = mix(occlusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
+            }
+        }
+    #endif
+
+    // outline needs to be handled after occlusion to keep them clean
+    #ifdef dOutlineEnable
+        float closestTexel;
+        float outline = getOutline(coords, closestTexel);
+
+        if (outline == 0.0) {
+            color.rgb *= outline;
+            viewDist = abs(getViewZ(closestTexel));
+            fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
+            if (!uTransparentBackground) {
+                color.rgb = mix(color.rgb, uFogColor, fogFactor);
+            } else {
+                color.a = 1.0 - fogFactor;
+            }
         }
     #endif
 

+ 66 - 0
src/mol-gl/shader/smaa/blend.frag.ts

@@ -0,0 +1,66 @@
+/**
+ * Slightly adapted from https://github.com/mrdoob/three.js
+ * MIT License Copyright (c) 2010-2020 three.js authors
+ *
+ * WebGL port of Subpixel Morphological Antialiasing (SMAA) v2.8
+ * Preset: SMAA 1x Medium (with color edge detection)
+ * https://github.com/iryoku/smaa/releases/tag/v2.8
+ */
+
+export const blend_frag = `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+uniform sampler2D tWeights;
+uniform sampler2D tColor;
+uniform vec2 uTexSizeInv;
+
+varying vec2 vUv;
+varying vec4 vOffset[2];
+
+vec4 SMAANeighborhoodBlendingPS(vec2 texCoord, vec4 offset[2], sampler2D colorTex, sampler2D blendTex) {
+    // Fetch the blending weights for current pixel:
+    vec4 a;
+    a.xz = texture2D(blendTex, texCoord).xz;
+    a.y = texture2D(blendTex, offset[1].zw).g;
+    a.w = texture2D(blendTex, offset[1].xy).a;
+
+    // Is there any blending weight with a value greater than 0.0?
+    if (dot(a, vec4(1.0, 1.0, 1.0, 1.0)) < 1e-5) {
+        return texture2D(colorTex, texCoord, 0.0);
+    } else {
+        // Up to 4 lines can be crossing a pixel (one through each edge). We
+        // favor blending by choosing the line with the maximum weight for each
+        // direction:
+        vec2 offset;
+        offset.x = a.a > a.b ? a.a : -a.b; // left vs. right
+        offset.y = a.g > a.r ? -a.g : a.r; // top vs. bottom // WebGL port note: Changed signs
+
+        // Then we go in the direction that has the maximum weight:
+        if (abs(offset.x) > abs(offset.y)) { // horizontal vs. vertical
+            offset.y = 0.0;
+        } else {
+            offset.x = 0.0;
+        }
+
+        // Fetch the opposite color and lerp by hand:
+        vec4 C = texture2D(colorTex, texCoord, 0.0);
+        texCoord += sign(offset) * uTexSizeInv;
+        vec4 Cop = texture2D(colorTex, texCoord, 0.0);
+        float s = abs(offset.x) > abs(offset.y) ? abs(offset.x) : abs(offset.y);
+
+        // WebGL port note: Added gamma correction
+        C.xyz = pow(C.xyz, vec3(2.2));
+        Cop.xyz = pow(Cop.xyz, vec3(2.2));
+        vec4 mixed = mix(C, Cop, s);
+        mixed.xyz = pow(mixed.xyz, vec3(1.0 / 2.2));
+
+        return mixed;
+    }
+}
+
+void main() {
+    gl_FragColor = SMAANeighborhoodBlendingPS(vUv, vOffset, tColor, tWeights);
+}
+`;

+ 35 - 0
src/mol-gl/shader/smaa/blend.vert.ts

@@ -0,0 +1,35 @@
+/**
+ * Slightly adapted from https://github.com/mrdoob/three.js
+ * MIT License Copyright (c) 2010-2020 three.js authors
+ *
+ * WebGL port of Subpixel Morphological Antialiasing (SMAA) v2.8
+ * Preset: SMAA 1x Medium (with color edge detection)
+ * https://github.com/iryoku/smaa/releases/tag/v2.8
+ */
+
+export const blend_vert = `
+precision highp float;
+
+attribute vec2 aPosition;
+uniform vec2 uQuadScale;
+
+uniform vec2 uTexSizeInv;
+uniform vec4 uViewport;
+
+varying vec2 vUv;
+varying vec4 vOffset[2];
+
+void SMAANeighborhoodBlendingVS(vec2 texCoord) {
+    vOffset[0] = texCoord.xyxy + uTexSizeInv.xyxy * vec4(-1.0, 0.0, 0.0, 1.0); // WebGL port note: Changed sign in W component
+    vOffset[1] = texCoord.xyxy + uTexSizeInv.xyxy * vec4(1.0, 0.0, 0.0, -1.0); // WebGL port note: Changed sign in W component
+}
+
+void main() {
+    vec2 scale = uViewport.zw * uTexSizeInv;
+    vec2 shift = uViewport.xy * uTexSizeInv;
+    vUv = (aPosition + 1.0) * 0.5 * scale + shift;
+    SMAANeighborhoodBlendingVS(vUv);
+    vec2 position = aPosition * uQuadScale - vec2(1.0, 1.0) + uQuadScale;
+    gl_Position = vec4(position, 0.0, 1.0);
+}
+`;

+ 76 - 0
src/mol-gl/shader/smaa/edges.frag.ts

@@ -0,0 +1,76 @@
+/**
+ * Slightly adapted from https://github.com/mrdoob/three.js
+ * MIT License Copyright (c) 2010-2020 three.js authors
+ *
+ * WebGL port of Subpixel Morphological Antialiasing (SMAA) v2.8
+ * Preset: SMAA 1x Medium (with color edge detection)
+ * https://github.com/iryoku/smaa/releases/tag/v2.8
+ */
+
+export const edges_frag = `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+uniform sampler2D tColor;
+uniform vec2 uTexSizeInv;
+
+varying vec2 vUv;
+varying vec4 vOffset[3];
+
+vec4 SMAAColorEdgeDetectionPS(vec2 texcoord, vec4 offset[3], sampler2D colorTex) {
+    vec2 threshold = vec2(dEdgeThreshold, dEdgeThreshold);
+
+    // Calculate color deltas:
+    vec4 delta;
+    vec3 C = texture2D(colorTex, texcoord).rgb;
+
+    vec3 Cleft = texture2D(colorTex, offset[0].xy).rgb;
+    vec3 t = abs(C - Cleft);
+    delta.x = max(max(t.r, t.g), t.b);
+
+    vec3 Ctop = texture2D(colorTex, offset[0].zw).rgb;
+    t = abs(C - Ctop);
+    delta.y = max(max(t.r, t.g), t.b);
+
+    // We do the usual threshold:
+    vec2 edges = step(threshold, delta.xy);
+
+    // Then discard if there is no edge:
+    if (dot(edges, vec2(1.0, 1.0 )) == 0.0)
+        discard;
+
+    // Calculate right and bottom deltas:
+    vec3 Cright = texture2D(colorTex, offset[1].xy).rgb;
+    t = abs( C - Cright );
+    delta.z = max(max(t.r, t.g), t.b);
+
+    vec3 Cbottom  = texture2D(colorTex, offset[1].zw).rgb;
+    t = abs(C - Cbottom);
+    delta.w = max(max(t.r, t.g), t.b);
+
+    // Calculate the maximum delta in the direct neighborhood:
+    float maxDelta = max(max(max(delta.x, delta.y), delta.z), delta.w );
+
+    // Calculate left-left and top-top deltas:
+    vec3 Cleftleft  = texture2D(colorTex, offset[2].xy).rgb;
+    t = abs( C - Cleftleft );
+    delta.z = max(max(t.r, t.g), t.b);
+
+    vec3 Ctoptop = texture2D(colorTex, offset[2].zw).rgb;
+    t = abs(C - Ctoptop);
+    delta.w = max(max(t.r, t.g), t.b);
+
+    // Calculate the final maximum delta:
+    maxDelta = max(max(maxDelta, delta.z), delta.w);
+
+    // Local contrast adaptation in action:
+    edges.xy *= step(0.5 * maxDelta, delta.xy);
+
+    return vec4(edges, 0.0, 0.0);
+}
+
+void main() {
+    gl_FragColor = SMAAColorEdgeDetectionPS(vUv, vOffset, tColor);
+}
+`;

+ 36 - 0
src/mol-gl/shader/smaa/edges.vert.ts

@@ -0,0 +1,36 @@
+/**
+ * Slightly adapted from https://github.com/mrdoob/three.js
+ * MIT License Copyright (c) 2010-2020 three.js authors
+ *
+ * WebGL port of Subpixel Morphological Antialiasing (SMAA) v2.8
+ * Preset: SMAA 1x Medium (with color edge detection)
+ * https://github.com/iryoku/smaa/releases/tag/v2.8
+ */
+
+export const edges_vert = `
+precision highp float;
+
+attribute vec2 aPosition;
+uniform vec2 uQuadScale;
+
+uniform vec2 uTexSizeInv;
+uniform vec4 uViewport;
+
+varying vec2 vUv;
+varying vec4 vOffset[3];
+
+void SMAAEdgeDetectionVS(vec2 texCoord) {
+    vOffset[0] = texCoord.xyxy + uTexSizeInv.xyxy * vec4(-1.0, 0.0, 0.0, 1.0); // WebGL port note: Changed sign in W component
+    vOffset[1] = texCoord.xyxy + uTexSizeInv.xyxy * vec4(1.0, 0.0, 0.0, -1.0); // WebGL port note: Changed sign in W component
+    vOffset[2] = texCoord.xyxy + uTexSizeInv.xyxy * vec4(-2.0, 0.0, 0.0, 2.0); // WebGL port note: Changed sign in W component
+}
+
+void main() {
+    vec2 scale = uViewport.zw * uTexSizeInv;
+    vec2 shift = uViewport.xy * uTexSizeInv;
+    vUv = (aPosition + 1.0) * 0.5 * scale + shift;
+    SMAAEdgeDetectionVS(vUv);
+    vec2 position = aPosition * uQuadScale - vec2(1.0, 1.0) + uQuadScale;
+    gl_Position = vec4(position, 0.0, 1.0);
+}
+`;

+ 216 - 0
src/mol-gl/shader/smaa/weights.frag.ts

@@ -0,0 +1,216 @@
+/**
+ * Slightly adapted from https://github.com/mrdoob/three.js
+ * MIT License Copyright (c) 2010-2020 three.js authors
+ *
+ * WebGL port of Subpixel Morphological Antialiasing (SMAA) v2.8
+ * Preset: SMAA 1x Medium (with color edge detection)
+ * https://github.com/iryoku/smaa/releases/tag/v2.8
+ */
+
+export const weights_frag = `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+#define SMAASampleLevelZeroOffset(tex, coord, offset) texture2D(tex, coord + float(offset) * uTexSizeInv, 0.0)
+
+#define SMAA_AREATEX_MAX_DISTANCE 16
+#define SMAA_AREATEX_PIXEL_SIZE (1.0 / vec2(160.0, 560.0))
+#define SMAA_AREATEX_SUBTEX_SIZE (1.0 / 7.0)
+
+uniform sampler2D tEdges;
+uniform sampler2D tArea;
+uniform sampler2D tSearch;
+uniform vec2 uTexSizeInv;
+
+varying vec2 vUv;
+varying vec4 vOffset[3];
+varying vec2 vPixCoord;
+
+#if __VERSION__ == 100
+    vec2 round(vec2 x) {
+        return sign(x) * floor(abs(x) + 0.5);
+    }
+#endif
+
+float SMAASearchLength(sampler2D searchTex, vec2 e, float bias, float scale) {
+    // Not required if searchTex accesses are set to point:
+    // float2 SEARCH_TEX_PIXEL_SIZE = 1.0 / float2(66.0, 33.0);
+    // e = float2(bias, 0.0) + 0.5 * SEARCH_TEX_PIXEL_SIZE +
+    //     e * float2(scale, 1.0) * float2(64.0, 32.0) * SEARCH_TEX_PIXEL_SIZE;
+    e.r = bias + e.r * scale;
+    return 255.0 * texture2D(searchTex, e, 0.0).r;
+}
+
+float SMAASearchXLeft(sampler2D edgesTex, sampler2D searchTex, vec2 texCoord, float end) {
+    /**
+     * @PSEUDO_GATHER4
+     * This texCoord has been offset by (-0.25, -0.125) in the vertex shader to
+     * sample between edge, thus fetching four edges in a row.
+     * Sampling with different offsets in each direction allows to disambiguate
+     * which edges are active from the four fetched ones.
+     */
+    vec2 e = vec2(0.0, 1.0);
+
+    for (int i = 0; i < dMaxSearchSteps; i++) { // WebGL port note: Changed while to for
+        e = texture2D( edgesTex, texCoord, 0.0).rg;
+        texCoord -= vec2(2.0, 0.0) * uTexSizeInv;
+        if (!(texCoord.x > end && e.g > 0.8281 && e.r == 0.0)) break;
+    }
+
+    // We correct the previous (-0.25, -0.125) offset we applied:
+    texCoord.x += 0.25 * uTexSizeInv.x;
+
+    // The searches are bias by 1, so adjust the coords accordingly:
+    texCoord.x += uTexSizeInv.x;
+
+    // Disambiguate the length added by the last step:
+    texCoord.x += 2.0 * uTexSizeInv.x; // Undo last step
+    texCoord.x -= uTexSizeInv.x * SMAASearchLength(searchTex, e, 0.0, 0.5);
+
+    return texCoord.x;
+}
+
+float SMAASearchXRight(sampler2D edgesTex, sampler2D searchTex, vec2 texCoord, float end) {
+    vec2 e = vec2( 0.0, 1.0 );
+
+    for (int i = 0; i < dMaxSearchSteps; i++) { // WebGL port note: Changed while to for
+        e = texture2D(edgesTex, texCoord, 0.0).rg;
+        texCoord += vec2(2.0, 0.0) * uTexSizeInv;
+        if (!(texCoord.x < end && e.g > 0.8281 && e.r == 0.0)) break;
+    }
+
+    texCoord.x -= 0.25 * uTexSizeInv.x;
+    texCoord.x -= uTexSizeInv.x;
+    texCoord.x -= 2.0 * uTexSizeInv.x;
+    texCoord.x += uTexSizeInv.x * SMAASearchLength( searchTex, e, 0.5, 0.5 );
+
+    return texCoord.x;
+}
+
+float SMAASearchYUp(sampler2D edgesTex, sampler2D searchTex, vec2 texCoord, float end) {
+    vec2 e = vec2( 1.0, 0.0 );
+
+    for (int i = 0; i < dMaxSearchSteps; i++) { // WebGL port note: Changed while to for
+        e = texture2D(edgesTex, texCoord, 0.0).rg;
+        texCoord += vec2(0.0, 2.0) * uTexSizeInv; // WebGL port note: Changed sign
+        if (!(texCoord.y > end && e.r > 0.8281 && e.g == 0.0)) break;
+    }
+
+    texCoord.y -= 0.25 * uTexSizeInv.y; // WebGL port note: Changed sign
+    texCoord.y -= uTexSizeInv.y; // WebGL port note: Changed sign
+    texCoord.y -= 2.0 * uTexSizeInv.y; // WebGL port note: Changed sign
+    texCoord.y += uTexSizeInv.y * SMAASearchLength(searchTex, e.gr, 0.0, 0.5); // WebGL port note: Changed sign
+
+    return texCoord.y;
+}
+
+float SMAASearchYDown(sampler2D edgesTex, sampler2D searchTex, vec2 texCoord, float end) {
+    vec2 e = vec2( 1.0, 0.0 );
+
+    for (int i = 0; i < dMaxSearchSteps; i++) { // WebGL port note: Changed while to for
+        e = texture2D(edgesTex, texCoord, 0.0).rg;
+        texCoord -= vec2( 0.0, 2.0 ) * uTexSizeInv; // WebGL port note: Changed sign
+        if (!(texCoord.y < end && e.r > 0.8281 && e.g == 0.0)) break;
+    }
+
+    texCoord.y += 0.25 * uTexSizeInv.y; // WebGL port note: Changed sign
+    texCoord.y += uTexSizeInv.y; // WebGL port note: Changed sign
+    texCoord.y += 2.0 * uTexSizeInv.y; // WebGL port note: Changed sign
+    texCoord.y -= uTexSizeInv.y * SMAASearchLength(searchTex, e.gr, 0.5, 0.5); // WebGL port note: Changed sign
+
+    return texCoord.y;
+}
+
+vec2 SMAAArea(sampler2D areaTex, vec2 dist, float e1, float e2, float offset) {
+    // Rounding prevents precision errors of bilinear filtering:
+    vec2 texCoord = float(SMAA_AREATEX_MAX_DISTANCE) * round(4.0 * vec2(e1, e2)) + dist;
+
+    // We do a scale and bias for mapping to texel space:
+    texCoord = SMAA_AREATEX_PIXEL_SIZE * texCoord + (0.5 * SMAA_AREATEX_PIXEL_SIZE);
+
+    // Move to proper place, according to the subpixel offset:
+    texCoord.y += SMAA_AREATEX_SUBTEX_SIZE * offset;
+
+    return texture2D(areaTex, texCoord, 0.0).rg;
+}
+
+vec4 SMAABlendingWeightCalculationPS(vec2 texCoord, vec2 pixCoord, vec4 offset[3], sampler2D edgesTex, sampler2D areaTex, sampler2D searchTex, ivec4 subsampleIndices) {
+    vec4 weights = vec4(0.0, 0.0, 0.0, 0.0);
+
+    vec2 e = texture2D(edgesTex, texCoord).rg;
+
+    if (e.g > 0.0) { // Edge at north
+        vec2 d;
+
+        // Find the distance to the left:
+        vec2 coords;
+        coords.x = SMAASearchXLeft(edgesTex, searchTex, offset[0].xy, offset[2].x );
+        coords.y = offset[1].y; // offset[1].y = texCoord.y - 0.25 * uTexSizeInv.y (@CROSSING_OFFSET)
+        d.x = coords.x;
+
+        // Now fetch the left crossing edges, two at a time using bilinear
+        // filtering. Sampling at -0.25 (see @CROSSING_OFFSET) enables to
+        // discern what value each edge has:
+        float e1 = texture2D(edgesTex, coords, 0.0).r;
+
+        // Find the distance to the right:
+        coords.x = SMAASearchXRight(edgesTex, searchTex, offset[0].zw, offset[2].y);
+        d.y = coords.x;
+
+        // We want the distances to be in pixel units (doing this here allow to
+        // better interleave arithmetic and memory accesses):
+        d = d / uTexSizeInv.x - pixCoord.x;
+
+        // SMAAArea below needs a sqrt, as the areas texture is compressed
+        // quadratically:
+        vec2 sqrt_d = sqrt(abs(d));
+
+        // Fetch the right crossing edges:
+        coords.y -= 1.0 * uTexSizeInv.y; // WebGL port note: Added
+        float e2 = SMAASampleLevelZeroOffset(edgesTex, coords, ivec2(1, 0)).r;
+
+        // Ok, we know how this pattern looks like, now it is time for getting
+        // the actual area:
+        weights.rg = SMAAArea(areaTex, sqrt_d, e1, e2, float(subsampleIndices.y));
+    }
+
+    if (e.r > 0.0) { // Edge at west
+        vec2 d;
+
+        // Find the distance to the top:
+        vec2 coords;
+
+        coords.y = SMAASearchYUp(edgesTex, searchTex, offset[1].xy, offset[2].z );
+        coords.x = offset[0].x; // offset[1].x = texCoord.x - 0.25 * uTexSizeInv.x;
+        d.x = coords.y;
+
+        // Fetch the top crossing edges:
+        float e1 = texture2D(edgesTex, coords, 0.0).g;
+
+        // Find the distance to the bottom:
+        coords.y = SMAASearchYDown(edgesTex, searchTex, offset[1].zw, offset[2].w);
+        d.y = coords.y;
+
+        // We want the distances to be in pixel units:
+        d = d / uTexSizeInv.y - pixCoord.y;
+
+        // SMAAArea below needs a sqrt, as the areas texture is compressed
+        // quadratically:
+        vec2 sqrt_d = sqrt(abs(d));
+
+        // Fetch the bottom crossing edges:
+        coords.y -= 1.0 * uTexSizeInv.y; // WebGL port note: Added
+        float e2 = SMAASampleLevelZeroOffset(edgesTex, coords, ivec2(0, 1)).g;
+
+        // Get the area for this direction:
+        weights.ba = SMAAArea(areaTex, sqrt_d, e1, e2, float(subsampleIndices.x));
+    }
+
+    return weights;
+}
+
+void main() {
+    gl_FragColor = SMAABlendingWeightCalculationPS(vUv, vPixCoord, vOffset, tEdges, tArea, tSearch, ivec4(0.0));
+}
+`;

+ 42 - 0
src/mol-gl/shader/smaa/weights.vert.ts

@@ -0,0 +1,42 @@
+/**
+ * Slightly adapted from https://github.com/mrdoob/three.js
+ * MIT License Copyright (c) 2010-2020 three.js authors
+ *
+ * WebGL port of Subpixel Morphological Antialiasing (SMAA) v2.8
+ * Preset: SMAA 1x Medium (with color edge detection)
+ * https://github.com/iryoku/smaa/releases/tag/v2.8
+ */
+
+export const weights_vert = `
+precision highp float;
+
+attribute vec2 aPosition;
+uniform vec2 uQuadScale;
+
+uniform vec2 uTexSizeInv;
+uniform vec4 uViewport;
+
+varying vec2 vUv;
+varying vec4 vOffset[3];
+varying vec2 vPixCoord;
+
+void SMAABlendingWeightCalculationVS(vec2 texCoord) {
+    vPixCoord = texCoord / uTexSizeInv;
+
+    // We will use these offsets for the searches later on (see @PSEUDO_GATHER4):
+    vOffset[0] = texCoord.xyxy + uTexSizeInv.xyxy * vec4(-0.25, 0.125, 1.25, 0.125); // WebGL port note: Changed sign in Y and W components
+    vOffset[1] = texCoord.xyxy + uTexSizeInv.xyxy * vec4(-0.125, 0.25, -0.125, -1.25); // WebGL port note: Changed sign in Y and W components
+
+    // And these for the searches, they indicate the ends of the loops:
+    vOffset[2] = vec4(vOffset[0].xz, vOffset[1].yw) + vec4(-2.0, 2.0, -2.0, 2.0) * uTexSizeInv.xxyy * float(dMaxSearchSteps);
+}
+
+void main() {
+    vec2 scale = uViewport.zw * uTexSizeInv;
+    vec2 shift = uViewport.xy * uTexSizeInv;
+    vUv = (aPosition + 1.0) * 0.5 * scale + shift;
+    SMAABlendingWeightCalculationVS(vUv);
+    vec2 position = aPosition * uQuadScale - vec2(1.0, 1.0) + uQuadScale;
+    gl_Position = vec4(position, 0.0, 1.0);
+}
+`;

+ 5 - 17
src/mol-gl/shader/spheres.frag.ts

@@ -15,9 +15,6 @@ precision highp int;
 #include common_clip
 #include wboit_params
 
-uniform float uClipNear;
-uniform float uIsOrtho;
-
 varying float vRadius;
 varying float vRadiusSq;
 varying vec3 vPoint;
@@ -26,10 +23,6 @@ varying vec3 vPointViewPosition;
 vec3 cameraPos;
 vec3 cameraNormal;
 
-float calcClip(const in vec3 cameraPos) {
-    return dot(vec4(cameraPos, 1.0), vec4(0.0, 0.0, 1.0, uClipNear - 0.5));
-}
-
 bool Impostor(out vec3 cameraPos, out vec3 cameraNormal){
     vec3 cameraSpherePos = -vPointViewPosition;
     cameraSpherePos.z += vRadius;
@@ -75,19 +68,17 @@ void main(void){
             discard;
     #endif
 
-    // FIXME not compatible with custom clipping plane
-    // Set the depth based on the new cameraPos.
-    gl_FragDepthEXT = calcDepth(cameraPos);
+    vec3 vViewPosition = cameraPos;
+    gl_FragDepthEXT = calcDepth(vViewPosition);
     if (!flag && gl_FragDepthEXT >= 0.0) {
         gl_FragDepthEXT = 0.0 + (0.0000001 / vRadius);
     }
 
     // bugfix (mac only?)
-    if (gl_FragDepthEXT < 0.0)
-        discard;
-    if (gl_FragDepthEXT > 1.0)
-        discard;
+    if (gl_FragDepthEXT < 0.0) discard;
+    if (gl_FragDepthEXT > 1.0) discard;
 
+    float fragmentDepth = gl_FragDepthEXT;
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)
@@ -100,15 +91,12 @@ void main(void){
             gl_FragColor = material;
         #else
             vec3 normal = -cameraNormal;
-            vec3 vViewPosition = cameraPos;
             #include apply_light_color
         #endif
 
         #include apply_interior_color
         #include apply_marker_color
         #include apply_fog
-
-        float fragmentDepth = gl_FragDepthEXT;
         #include wboit_write
     #endif
 }

+ 84 - 0
src/mol-gl/shader/ssao-blur.frag.ts

@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+uniform sampler2D tSsaoDepth;
+uniform vec2 uTexSize;
+
+uniform float uKernel[dOcclusionKernelSize];
+
+uniform float uBlurDirectionX;
+uniform float uBlurDirectionY;
+
+uniform float uMaxPossibleViewZDiff;
+
+uniform float uNear;
+uniform float uFar;
+
+#include common
+
+float getViewZ(const in float depth) {
+    #if dOrthographic == 1
+        return orthographicDepthToViewZ(depth, uNear, uFar);
+    #else
+        return perspectiveDepthToViewZ(depth, uNear, uFar);
+    #endif
+}
+
+bool isBackground(const in float depth) {
+    return depth == 1.0;
+}
+
+void main(void) {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+
+    vec2 packedDepth = texture2D(tSsaoDepth, coords).zw;
+
+    float selfDepth = unpackRGToUnitInterval(packedDepth);
+    // if background and if second pass
+    if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
+       gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
+       return;
+    }
+
+    float selfViewZ = getViewZ(selfDepth);
+
+    vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
+
+    float sum = 0.0;
+    float kernelSum = 0.0;
+    // only if kernelSize is odd
+    for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
+        vec2 sampleCoords = coords + float(i) * offset;
+
+        vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords);
+
+        float sampleDepth = unpackRGToUnitInterval(sampleSsaoDepth.zw);
+        if (isBackground(sampleDepth)) {
+            continue;
+        }
+
+        if (abs(float(i)) > 1.0) { // abs is not defined for int in webgl1
+            float sampleViewZ = getViewZ(sampleDepth);
+            if (abs(selfViewZ - sampleViewZ) > uMaxPossibleViewZDiff) {
+                continue;
+            }
+        }
+
+        float kernel = uKernel[int(abs(float(i)))]; // abs is not defined for int in webgl1
+        float sampleValue = unpackRGToUnitInterval(sampleSsaoDepth.xy);
+
+        sum += kernel * sampleValue;
+        kernelSum += kernel;
+    }
+
+    gl_FragColor = vec4(packUnitIntervalToRG(sum / kernelSum), packedDepth);
+}
+`;

+ 110 - 0
src/mol-gl/shader/ssao.frag.ts

@@ -0,0 +1,110 @@
+/**
+ * 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>
+ */
+
+export default `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+#include common
+
+uniform sampler2D tDepth;
+
+uniform vec3 uSamples[dNSamples];
+
+uniform mat4 uProjection;
+uniform mat4 uInvProjection;
+
+uniform vec2 uTexSize;
+
+uniform float uRadius;
+uniform float uBias;
+
+float smootherstep(float edge0, float edge1, float x) {
+    x = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
+    return x * x * x * (x * (x * 6.0 - 15.0) + 10.0);
+}
+
+float noise(const in vec2 coords) {
+    float a = 12.9898;
+    float b = 78.233;
+    float c = 43758.5453;
+    float dt = dot(coords, vec2(a,b));
+    float sn = mod(dt, PI);
+    return abs(fract(sin(sn) * c)); // is abs necessary?
+}
+
+vec2 getNoiseVec2(const in vec2 coords) {
+    return vec2(noise(coords), noise(coords + vec2(PI, 2.71828)));
+}
+
+bool isBackground(const in float depth) {
+    return depth == 1.0;
+}
+
+float getDepth(const in vec2 coords) {
+    return unpackRGBAToDepth(texture2D(tDepth, coords));
+}
+
+vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {
+    vec3 p1 = vec3(offset1, depth1 - depth);
+    vec3 p2 = vec3(offset2, depth2 - depth);
+
+    vec3 normal = cross(p1, p2);
+    normal.z = -normal.z;
+
+    return normalize(normal);
+}
+
+// StarCraft II Ambient Occlusion by [Filion and McNaughton 2008]
+void main(void) {
+    vec2 invTexSize = 1.0 / uTexSize;
+    vec2 selfCoords = gl_FragCoord.xy * invTexSize;
+
+    float selfDepth = getDepth(selfCoords);
+    vec2 selfPackedDepth = packUnitIntervalToRG(selfDepth);
+
+    if (isBackground(selfDepth)) {
+        gl_FragColor = vec4(packUnitIntervalToRG(0.0), selfPackedDepth);
+        return;
+    }
+
+    vec2 offset1 = vec2(0.0, invTexSize.y);
+    vec2 offset2 = vec2(invTexSize.x, 0.0);
+
+    float selfDepth1 = getDepth(selfCoords + offset1);
+    float selfDepth2 = getDepth(selfCoords + offset2);
+
+    vec3 selfViewNormal = normalFromDepth(selfDepth, selfDepth1, selfDepth2, offset1, offset2);
+    vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
+
+    vec3 randomVec = normalize(vec3(getNoiseVec2(selfCoords) * 2.0 - 1.0, 0.0));
+
+    vec3 tangent = normalize(randomVec - selfViewNormal * dot(randomVec, selfViewNormal));
+    vec3 bitangent = cross(selfViewNormal, tangent);
+    mat3 TBN = mat3(tangent, bitangent, selfViewNormal);
+
+    float occlusion = 0.0;
+    for(int i = 0; i < dNSamples; i++){
+        vec3 sampleViewPos = TBN * uSamples[i];
+        sampleViewPos = selfViewPos + sampleViewPos * uRadius;
+
+        vec4 offset = vec4(sampleViewPos, 1.0);
+        offset = uProjection * offset;
+        offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
+
+        float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, getDepth(offset.xy)), uInvProjection).z;
+
+        occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
+    }
+    occlusion = 1.0 - (uBias * occlusion / float(dNSamples));
+
+    vec2 packedOcclusion = packUnitIntervalToRG(occlusion);
+
+    gl_FragColor = vec4(packedOcclusion, selfPackedDepth);
+}
+`;

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

@@ -32,6 +32,9 @@ void main2(){
 
 void main(){
     #include clip_pixel
+
+    float fragmentDepth = gl_FragCoord.z;
+    bool interior = false;
     #include assign_material_color
 
     if (vTexCoord.x > 1.0) {
@@ -66,9 +69,6 @@ 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
 }

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

@@ -19,7 +19,7 @@ export interface RenderTarget {
 
     getWidth: () => number
     getHeight: () => number
-    /** binds framebuffer and sets viewport to rendertarget's width and height */
+    /** binds framebuffer */
     bind: () => void
     setSize: (width: number, height: number) => void
     reset: () => void

+ 25 - 4
src/mol-gl/webgl/texture.ts

@@ -151,6 +151,10 @@ export function getAttachment(gl: GLRenderingContext, extensions: WebGLExtension
     throw new Error('unknown texture attachment');
 }
 
+function isImage(x: TextureImage<any> | TextureVolume<any> | HTMLImageElement): x is HTMLImageElement {
+    return typeof HTMLImageElement !== undefined && (x instanceof HTMLImageElement);
+}
+
 function isTexture2d(x: TextureImage<any> | TextureVolume<any>, target: number, gl: GLRenderingContext): x is TextureImage<any> {
     return target === gl.TEXTURE_2D;
 }
@@ -177,7 +181,7 @@ export interface Texture {
      * The `sub` option requires an existing allocation on the GPU, that is, either
      * `define` or `load` without `sub` must have been called before.
      */
-    load: (image: TextureImage<any> | TextureVolume<any>, sub?: boolean) => void
+    load: (image: TextureImage<any> | TextureVolume<any> | HTMLImageElement, sub?: boolean) => void
     bind: (id: TextureId) => void
     unbind: (id: TextureId) => void
     /** Use `layer` to attach a z-slice of a 3D texture */
@@ -233,7 +237,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
     init();
 
     let width = 0, height = 0, depth = 0;
-    let loadedData: undefined | TextureImage<any> | TextureVolume<any>;
+    let loadedData: undefined | TextureImage<any> | TextureVolume<any> | HTMLImageElement;
     let destroyed = false;
 
     function define(_width: number, _height: number, _depth?: number) {
@@ -250,13 +254,17 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
         }
     }
 
-    function load(data: TextureImage<any> | TextureVolume<any>, sub = false) {
+    function load(data: TextureImage<any> | TextureVolume<any> | HTMLImageElement, sub = false) {
         gl.bindTexture(target, texture);
         // unpack alignment of 1 since we use textures only for data
         gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
         gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
         gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
-        if (isTexture2d(data, target, gl)) {
+        if (isImage(data)) {
+            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
+            gl.bindTexture(gl.TEXTURE_2D, texture);
+            gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, format, type, data);
+        } else if (isTexture2d(data, target, gl)) {
             gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !!data.flipY);
             if (sub) {
                 gl.texSubImage2D(target, 0, 0, 0, data.width, data.height, format, type, data.array);
@@ -363,6 +371,19 @@ export function createTextures(ctx: WebGLContext, schema: RenderableSchema, valu
     return textures;
 }
 
+/**
+ * Loads an image from a url to a textures and triggers update asynchronously.
+ * This will not work on node.js with a polyfill for HTMLImageElement.
+ */
+export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture: Texture) {
+    const img = new Image();
+    img.onload = function() {
+        texture.load(img);
+        ValueCell.update(cell, texture);
+    };
+    img.src = src;
+}
+
 //
 
 export function createNullTexture(gl: GLRenderingContext, kind: TextureKind): Texture {

+ 10 - 35
src/mol-math/geometry/gaussian-density.ts

@@ -1,36 +1,22 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Box3D, DensityData, DensityTextureData } from '../geometry';
-import { RuntimeContext, Task } from '../../mol-task';
 import { PositionData } from './common';
-import { GaussianDensityCPU } from './gaussian-density/cpu';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { Texture } from '../../mol-gl/webgl/texture';
 import { GaussianDensityTexture2d, GaussianDensityTexture3d } from './gaussian-density/gpu';
+import { Task } from '../../mol-task/task';
+import { GaussianDensityCPU } from './gaussian-density/cpu';
 
-// import { GaussianDensityGPU, GaussianDensityTexture } from './gaussian-density/gpu';
-const GaussianDensityGPU = typeof document !== 'undefined'
-    ? (require('./gaussian-density/gpu') as typeof import('./gaussian-density/gpu')).GaussianDensityGPU
-    : void 0;
-const GaussianDensityTexture = typeof document !== 'undefined'
-    ? (require('./gaussian-density/gpu') as typeof import('./gaussian-density/gpu')).GaussianDensityTexture
-    : void 0;
-
-export const DefaultGaussianDensityGPUProps = {
+export const DefaultGaussianDensityProps = {
     resolution: 1,
     radiusOffset: 0,
     smoothness: 1.5,
 };
-export type GaussianDensityGPUProps = typeof DefaultGaussianDensityGPUProps
-
-export const DefaultGaussianDensityProps = {
-    ...DefaultGaussianDensityGPUProps,
-    useGpu: true,
-};
 export type GaussianDensityProps = typeof DefaultGaussianDensityProps
 
 export type GaussianDensityData = {
@@ -41,36 +27,25 @@ export type GaussianDensityTextureData = {
     radiusFactor: number
 } & DensityTextureData
 
-export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps, webgl?: WebGLContext) {
+export function computeGaussianDensity(position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps) {
     return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensity(ctx, position, box, radius, props, webgl);
-    });
-}
-
-export async function GaussianDensity(ctx: RuntimeContext, position: PositionData, box: Box3D, radius: (index: number) => number,  props: GaussianDensityProps, webgl?: WebGLContext): Promise<GaussianDensityData> {
-    if (props.useGpu) {
-        if (!GaussianDensityGPU) throw 'GPU computation not supported on this platform';
-        if (!webgl) throw 'No WebGL context provided';
-        return GaussianDensityGPU(position, box, radius, props, webgl);
-    } else {
         return await GaussianDensityCPU(ctx, position, box, radius, props);
-    }
+    });
 }
 
-export function computeGaussianDensityTexture(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
+export function computeGaussianDensityTexture(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     return _computeGaussianDensityTexture(webgl.isWebGL2 ? '3d' : '2d', position, box, radius, props, webgl, texture);
 }
 
-export function computeGaussianDensityTexture2d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
+export function computeGaussianDensityTexture2d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     return _computeGaussianDensityTexture('2d', position, box, radius, props, webgl, texture);
 }
 
-export function computeGaussianDensityTexture3d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
+export function computeGaussianDensityTexture3d(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     return _computeGaussianDensityTexture('2d', position, box, radius, props, webgl, texture);
 }
 
-function _computeGaussianDensityTexture(type: '2d' | '3d', position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext, texture?: Texture) {
-    if (!GaussianDensityTexture) throw 'GPU computation not supported on this platform';
+function _computeGaussianDensityTexture(type: '2d' | '3d', position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     return Task.create('Gaussian Density', async ctx => {
         return type === '2d' ?
             GaussianDensityTexture2d(webgl, position, box, radius, false, props, texture) :

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

@@ -7,7 +7,7 @@
 
 import { PositionData } from '../common';
 import { Box3D } from '../../geometry';
-import { GaussianDensityGPUProps, GaussianDensityData, GaussianDensityTextureData } from '../gaussian-density';
+import { GaussianDensityProps, GaussianDensityData, GaussianDensityTextureData } from '../gaussian-density';
 import { OrderedSet } from '../../../mol-data/int';
 import { Vec3, Tensor, Mat4, Vec2 } from '../../linear-algebra';
 import { ValueCell } from '../../../mol-util';
@@ -68,7 +68,7 @@ function getTexture(name: string, webgl: WebGLContext, kind: TextureKind, format
     return webgl.namedTextures[_name];
 }
 
-export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, webgl: WebGLContext): GaussianDensityData {
+export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, webgl: WebGLContext): GaussianDensityData {
     // always use texture2d when the gaussian density needs to be downloaded from the GPU,
     // it's faster than texture3d
     // console.time('GaussianDensityTexture2d')
@@ -81,17 +81,17 @@ export function GaussianDensityGPU(position: PositionData, box: Box3D, radius: (
     return { field, idField, transform: getTransform(scale, bbox), radiusFactor };
 }
 
-export function GaussianDensityTexture(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): GaussianDensityTextureData {
+export function GaussianDensityTexture(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, oldTexture?: Texture): GaussianDensityTextureData {
     return webgl.isWebGL2 ?
         GaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture) :
         GaussianDensityTexture2d(webgl, position, box, radius, false, props, oldTexture);
 }
 
-export function GaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityGPUProps, oldTexture?: Texture): GaussianDensityTextureData {
+export function GaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityProps, oldTexture?: Texture): GaussianDensityTextureData {
     return finalizeGaussianDensityTexture(calcGaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, props, oldTexture));
 }
 
-export function GaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, oldTexture?: Texture): GaussianDensityTextureData {
+export function GaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, oldTexture?: Texture): GaussianDensityTextureData {
     return finalizeGaussianDensityTexture(calcGaussianDensityTexture3d(webgl, position, box, radius, props, oldTexture));
 }
 
@@ -118,7 +118,7 @@ type _GaussianDensityTextureData = {
     radiusFactor: number
 }
 
-function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityGPUProps, texture?: Texture): _GaussianDensityTextureData {
+function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, powerOfTwo: boolean, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
     // console.log('2d');
     const { gl, resources, state, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = webgl;
     const { smoothness, resolution } = props;
@@ -201,7 +201,7 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
     return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor };
 }
 
-function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps, texture?: Texture): _GaussianDensityTextureData {
+function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps, texture?: Texture): _GaussianDensityTextureData {
     // console.log('3d');
     const { gl, resources, state, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = webgl;
     const { smoothness, resolution } = props;
@@ -259,7 +259,7 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
 
 //
 
-function prepareGaussianDensityData(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityGPUProps) {
+function prepareGaussianDensityData(position: PositionData, box: Box3D, radius: (index: number) => number, props: GaussianDensityProps) {
     const { resolution, radiusOffset } = props;
     const scaleFactor = 1 / resolution;
 

+ 5 - 0
src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts

@@ -80,6 +80,8 @@ export const InteractionsInterUnitParams = {
     ...ComplexMeshParams,
     ...LinkCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
+    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
 };
 export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
 
@@ -93,6 +95,9 @@ export function InteractionsInterUnitVisual(materialId: number): ComplexVisual<I
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InteractionsInterUnitParams>, currentProps: PD.Values<InteractionsInterUnitParams>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.dashCount !== currentProps.dashCount ||
+                newProps.dashScale !== currentProps.dashScale ||
+                newProps.dashCap !== currentProps.dashCap ||
                 newProps.radialSegments !== currentProps.radialSegments
             );
 

+ 5 - 0
src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts

@@ -66,6 +66,8 @@ export const InteractionsIntraUnitParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
+    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
 };
 export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
 
@@ -79,6 +81,9 @@ export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<Int
         setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InteractionsIntraUnitParams>, currentProps: PD.Values<InteractionsIntraUnitParams>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
             state.createGeometry = (
                 newProps.sizeFactor !== currentProps.sizeFactor ||
+                newProps.dashCount !== currentProps.dashCount ||
+                newProps.dashScale !== currentProps.dashScale ||
+                newProps.dashCap !== currentProps.dashCap ||
                 newProps.radialSegments !== currentProps.radialSegments
             );
 

+ 11 - 3
src/mol-model-props/computed/themes/accessible-surface-area.ts

@@ -9,7 +9,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Color, ColorScale } from '../../../mol-util/color';
 import { ThemeDataContext } from '../../../mol-theme/theme';
 import { ColorTheme, LocationColor } from '../../../mol-theme/color';
-import { StructureElement, Unit } from '../../../mol-model/structure';
+import { Bond, StructureElement, Unit } from '../../../mol-model/structure';
 import { AccessibleSurfaceAreaProvider } from '../accessible-surface-area';
 import { AccessibleSurfaceArea } from '../accessible-surface-area/shrake-rupley';
 import { CustomProperty } from '../../common/custom-property';
@@ -40,12 +40,20 @@ export function AccessibleSurfaceAreaColorTheme(ctx: ThemeDataContext, props: PD
     const contextHash = accessibleSurfaceArea ? hash2(accessibleSurfaceArea.id, accessibleSurfaceArea.version) : -1;
 
     if (accessibleSurfaceArea?.value && ctx.structure) {
+        const l = StructureElement.Location.create(ctx.structure);
         const asa = accessibleSurfaceArea.value;
+        const getColor = (location: StructureElement.Location) => {
+            const value = AccessibleSurfaceArea.getNormalizedValue(location, asa);
+            return value === -1 ? DefaultColor : scale.color(value);
+        };
 
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location) && Unit.isAtomic(location.unit)) {
-                const value = AccessibleSurfaceArea.getNormalizedValue(location, asa);
-                return value === -1 ? DefaultColor : scale.color(value);
+                return getColor(location);
+            } else if (Bond.isLocation(location)) {
+                l.unit = location.aUnit;
+                l.element = location.aUnit.elements[location.aIndex];
+                return getColor(l);
             }
             return DefaultColor;
         };

+ 8 - 4
src/mol-model/custom-property.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -9,7 +9,6 @@ import { CifWriter } from '../mol-io/writer/cif';
 import { CifExportContext } from './structure/export/mmcif';
 import { QuerySymbolRuntime } from '../mol-script/runtime/query/compiler';
 import { UUID } from '../mol-util';
-import { Asset } from '../mol-util/assets';
 
 export { CustomPropertyDescriptor, CustomProperties };
 
@@ -40,11 +39,16 @@ namespace CustomPropertyDescriptor {
     }
 }
 
+/**
+ * Anything with a dispose method, used to despose of data assets or webgl resources
+ */
+type Asset = { dispose: () => void }
+
 class CustomProperties {
     private _list: CustomPropertyDescriptor[] = [];
     private _set = new Set<CustomPropertyDescriptor>();
     private _refs = new Map<CustomPropertyDescriptor, number>();
-    private _assets = new Map<CustomPropertyDescriptor, Asset.Wrapper[]>();
+    private _assets = new Map<CustomPropertyDescriptor, Asset[]>();
 
     get all(): ReadonlyArray<CustomPropertyDescriptor> {
         return this._list;
@@ -72,7 +76,7 @@ class CustomProperties {
     }
 
     /** Sets assets for a prop, disposes of existing assets for that prop */
-    assets(desc: CustomPropertyDescriptor<any>, assets?: Asset.Wrapper[]) {
+    assets(desc: CustomPropertyDescriptor<any>, assets?: Asset[]) {
         const prevAssets = this._assets.get(desc);
         if (prevAssets) {
             for (const a of prevAssets) a.dispose();

+ 1 - 3
src/mol-plugin-state/transforms/representation.ts

@@ -653,9 +653,7 @@ const VolumeRepresentation3D = PluginStateTransform.BuiltIn({
         return Task.create('Volume Representation', async ctx => {
             if (newParams.type.name !== oldParams.type.name) {
                 const oldProvider = plugin.representation.volume.registry.get(oldParams.type.name);
-                if (oldProvider.ensureCustomProperties) {
-                    oldProvider.ensureCustomProperties.detach(a.data);
-                }
+                oldProvider.ensureCustomProperties?.detach(a.data);
                 return StateTransformer.UpdateResult.Recreate;
             }
             const props = { ...b.data.repr.props, ...newParams.type.params };

+ 15 - 0
src/mol-plugin-state/transforms/volume.ts

@@ -46,6 +46,9 @@ const VolumeFromCcp4 = PluginStateTransform.BuiltIn({
             const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.NX}\u00D7${a.data.header.NX}\u00D7${a.data.header.NX}` };
             return new SO.Volume.Data(volume, props);
         });
+    },
+    dispose({ b }) {
+        b?.data.customProperties.dispose();
     }
 });
 
@@ -68,6 +71,9 @@ const VolumeFromDsn6 = PluginStateTransform.BuiltIn({
             const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.xExtent}\u00D7${a.data.header.yExtent}\u00D7${a.data.header.zExtent}` };
             return new SO.Volume.Data(volume, props);
         });
+    },
+    dispose({ b }) {
+        b?.data.customProperties.dispose();
     }
 });
 
@@ -91,6 +97,9 @@ const VolumeFromCube = PluginStateTransform.BuiltIn({
             const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.dim[0]}\u00D7${a.data.header.dim[1]}\u00D7${a.data.header.dim[2]}` };
             return new SO.Volume.Data(volume, props);
         });
+    },
+    dispose({ b }) {
+        b?.data.customProperties.dispose();
     }
 });
 
@@ -107,6 +116,9 @@ const VolumeFromDx = PluginStateTransform.BuiltIn({
             const props = { label: volume.label || 'Volume', description: `Volume ${a.data.header.dim[0]}\u00D7${a.data.header.dim[1]}\u00D7${a.data.header.dim[2]}` };
             return new SO.Volume.Data(volume, props);
         });
+    },
+    dispose({ b }) {
+        b?.data.customProperties.dispose();
     }
 });
 
@@ -142,6 +154,9 @@ const VolumeFromDensityServerCif = PluginStateTransform.BuiltIn({
             const props = { label: densityServerCif.volume_data_3d_info.name.value(0), description: `Volume ${x}\u00D7${y}\u00D7${z}` };
             return new SO.Volume.Data(volume, props);
         });
+    },
+    dispose({ b }) {
+        b?.data.customProperties.dispose();
     }
 });
 

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

@@ -27,7 +27,7 @@ export const PluginConfig = {
         DisablePreserveDrawingBuffer: item('plugin-config.disable-preserve-drawing-buffer', false),
         PixelScale: item('plugin-config.pixel-scale', 1),
         PickScale: item('plugin-config.pick-scale', 0.25),
-        EnableWboit: item('plugin-config.enable-wboit', false),
+        EnableWboit: item('plugin-config.enable-wboit', true),
     },
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),

+ 11 - 8
src/mol-repr/structure/complex-representation.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -20,9 +20,11 @@ import { Overpaint } from '../../mol-theme/overpaint';
 import { StructureParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
 import { Transparency } from '../../mol-theme/transparency';
+import { WebGLContext } from '../../mol-gl/webgl/context';
 
-export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => ComplexVisual<P>): StructureRepresentation<P> {
+export function ComplexRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, props?: PD.Values<P>, webgl?: WebGLContext) => ComplexVisual<P>): StructureRepresentation<P> {
     let version = 0;
+    const { webgl } = ctx;
     const updated = new Subject<number>();
     const materialId = getNextMaterialId();
     const renderObjects: GraphicsRenderObject[] = [];
@@ -45,15 +47,16 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         return Task.create('Creating or updating ComplexRepresentation', async runtime => {
             let newVisual = false;
             if (!visual) {
-                visual = visualCtor(materialId);
+                visual = visualCtor(materialId, _props, webgl);
+                newVisual = true;
+            } else if (visual.mustRecreate?.(_props, webgl)) {
+                visual.destroy();
+                visual = visualCtor(materialId, _props, webgl);
                 newVisual = true;
             }
-            const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, structure);
+            const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, structure);
             if (promise) await promise;
-            if (newVisual) {
-                // ensure state is current for new visual
-                setState(_state);
-            }
+            if (newVisual) setState(_state); // current state for new visual
             // update list of renderObjects
             renderObjects.length = 0;
             if (visual && visual.renderObject) renderObjects.push(visual.renderObject);

+ 27 - 5
src/mol-repr/structure/complex-visual.ts

@@ -25,13 +25,14 @@ import { Mat4 } from '../../mol-math/linear-algebra';
 import { Overpaint } from '../../mol-theme/overpaint';
 import { Transparency } from '../../mol-theme/transparency';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
+import { Cylinders } from '../../mol-geo/geometry/cylinders/cylinders';
+import { Lines } from '../../mol-geo/geometry/lines/lines';
 import { Text } from '../../mol-geo/geometry/text/text';
 import { SizeTheme } from '../../mol-theme/size';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 import { createMarkers } from '../../mol-geo/geometry/marker-data';
-import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams, StructureTextureMeshParams } from './params';
+import { StructureParams, StructureMeshParams, StructureTextParams, StructureDirectVolumeParams, StructureLinesParams, StructureCylindersParams, StructureTextureMeshParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
-import { Lines } from '../../mol-geo/geometry/lines/lines';
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 
 export interface  ComplexVisual<P extends StructureParams> extends Visual<Structure, P> { }
@@ -51,6 +52,8 @@ interface ComplexVisualBuilder<P extends StructureParams, G extends Geometry> {
     getLoci(pickingId: PickingId, structure: Structure, id: number): Loci
     eachLocation(loci: Loci, structure: Structure, apply: (interval: Interval) => boolean, isMarking: boolean): boolean,
     setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure): void
+    mustRecreate?: (props: PD.Values<P>) => boolean
+    dispose?: (geometry: G) => void
 }
 
 interface ComplexVisualGeometryBuilder<P extends StructureParams, G extends Geometry> extends ComplexVisualBuilder<P, G> {
@@ -58,7 +61,7 @@ interface ComplexVisualGeometryBuilder<P extends StructureParams, G extends Geom
 }
 
 export function ComplexVisual<G extends Geometry, P extends StructureParams & Geometry.Params<G>>(builder: ComplexVisualGeometryBuilder<P, G>, materialId: number): ComplexVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder;
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, dispose } = builder;
     const { updateValues, updateBoundingSphere, updateRenderableState, createPositionIterator } = builder.geometryUtils;
     const updateState = VisualUpdateState.create();
 
@@ -238,9 +241,10 @@ export function ComplexVisual<G extends Geometry, P extends StructureParams & Ge
             Visual.setClipping(renderObject, clipping, lociApply, true);
         },
         destroy() {
-            // TODO
+            dispose?.(geometry);
             renderObject = undefined;
-        }
+        },
+        mustRecreate
     };
 }
 
@@ -262,6 +266,24 @@ export function ComplexMeshVisual<P extends ComplexMeshParams>(builder: ComplexM
     }, materialId);
 }
 
+// cylinders
+
+export const ComplexCylindersParams = { ...StructureCylindersParams, ...StructureParams };
+export type ComplexCylindersParams = typeof ComplexCylindersParams
+
+export interface ComplexCylindersVisualBuilder<P extends ComplexCylindersParams> extends ComplexVisualBuilder<P, Cylinders> { }
+
+export function ComplexCylindersVisual<P extends ComplexCylindersParams>(builder: ComplexCylindersVisualBuilder<P>, materialId: number): ComplexVisual<P> {
+    return ComplexVisual<Cylinders, P>({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructure: Structure, currentStructure: Structure) => {
+            builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme, newStructure, currentStructure);
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true;
+        },
+        geometryUtils: Cylinders.Utils
+    }, materialId);
+}
+
 // lines
 
 export const ComplexLinesParams = { ...StructureLinesParams, ...StructureParams };

+ 4 - 1
src/mol-repr/structure/params.ts

@@ -10,6 +10,7 @@ import { Lines } from '../../mol-geo/geometry/lines/lines';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { Points } from '../../mol-geo/geometry/points/points';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
+import { Cylinders } from '../../mol-geo/geometry/cylinders/cylinders';
 import { Text } from '../../mol-geo/geometry/text/text';
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
@@ -30,10 +31,12 @@ export type StructureMeshParams = typeof StructureMeshParams
 export const StructureSpheresParams = { ...Spheres.Params };
 export type StructureSpheresParams = typeof StructureSpheresParams
 
+export const StructureCylindersParams = { ...Cylinders.Params };
+export type StructureCylindersParams = typeof StructureCylindersParams
+
 export const StructurePointsParams = { ...Points.Params };
 export type StructurePointsParams = typeof StructurePointsParams
 
-
 export const StructureLinesParams = { ...Lines.Params };
 export type StructureLinesParams = typeof StructureLinesParams
 

+ 4 - 4
src/mol-repr/structure/representation/ball-and-stick.ts

@@ -1,12 +1,12 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { getElementSphereVisual, ElementSphereParams } from '../visual/element-sphere';
 import { IntraUnitBondCylinderVisual, IntraUnitBondCylinderParams } from '../visual/bond-intra-unit-cylinder';
-import { InterUnitBondCylinderVisual, InterUnitBondCylinderParams } from '../visual/bond-inter-unit-cylinder';
+import { InterUnitBondCylinderParams, InterUnitBondCylinderVisual } from '../visual/bond-inter-unit-cylinder';
+import { ElementSphereVisual, ElementSphereParams } from '../visual/element-sphere';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { UnitsRepresentation } from '../units-representation';
 import { ComplexRepresentation } from '../complex-representation';
@@ -17,7 +17,7 @@ import { Structure } from '../../../mol-model/structure';
 import { getUnitKindsParam } from '../params';
 
 const BallAndStickVisuals = {
-    'element-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Element sphere mesh', ctx, getParams, getElementSphereVisual(ctx.webgl)),
+    'element-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Element sphere', ctx, getParams, ElementSphereVisual),
     'intra-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, IntraUnitBondCylinderParams>) => UnitsRepresentation('Intra-unit bond cylinder', ctx, getParams, IntraUnitBondCylinderVisual),
     'inter-bond': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, InterUnitBondCylinderParams>) => ComplexRepresentation('Inter-unit bond cylinder', ctx, getParams, InterUnitBondCylinderVisual),
 };

+ 2 - 2
src/mol-repr/structure/representation/ellipsoid.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -12,7 +12,7 @@ import { UnitsRepresentation, StructureRepresentation, StructureRepresentationSt
 import { EllipsoidMeshParams, EllipsoidMeshVisual } from '../visual/ellipsoid-mesh';
 import { AtomSiteAnisotrop } from '../../../mol-model-formats/structure/property/anisotropic';
 import { IntraUnitBondCylinderParams, IntraUnitBondCylinderVisual } from '../visual/bond-intra-unit-cylinder';
-import { InterUnitBondCylinderParams, InterUnitBondCylinderVisual } from '../visual/bond-inter-unit-cylinder';
+import { InterUnitBondCylinderVisual, InterUnitBondCylinderParams } from '../visual/bond-inter-unit-cylinder';
 import { getUnitKindsParam } from '../params';
 
 const EllipsoidVisuals = {

+ 3 - 6
src/mol-repr/structure/representation/gaussian-surface.ts

@@ -4,7 +4,7 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { GaussianSurfaceMeshVisual, GaussianSurfaceTextureMeshVisual, GaussianSurfaceMeshParams, StructureGaussianSurfaceMeshParams, StructureGaussianSurfaceMeshVisual } from '../visual/gaussian-surface-mesh';
+import { GaussianSurfaceMeshParams, StructureGaussianSurfaceMeshParams, StructureGaussianSurfaceVisual, GaussianSurfaceVisual } from '../visual/gaussian-surface-mesh';
 import { UnitsRepresentation } from '../units-representation';
 import { GaussianWireframeVisual, GaussianWireframeParams } from '../visual/gaussian-surface-wireframe';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -14,11 +14,8 @@ import { ThemeRegistryContext } from '../../../mol-theme/theme';
 import { Structure } from '../../../mol-model/structure';
 
 const GaussianSurfaceVisuals = {
-    'gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface mesh', ctx, getParams, GaussianSurfaceMeshVisual),
-    'structure-gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, StructureGaussianSurfaceMeshParams>) => ComplexRepresentation('Structure-Gaussian surface mesh', ctx, getParams, StructureGaussianSurfaceMeshVisual),
-    'gaussian-surface-texture-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface texture-mesh', ctx, getParams, GaussianSurfaceTextureMeshVisual),
-    // TODO: don't enable yet as it breaks state sessions
-    // 'structure-gaussian-surface-texture-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, StructureGaussianSurfaceMeshParams>) => ComplexRepresentation('Structure-Gaussian surface texture-mesh', ctx, getParams, StructureGaussianSurfaceTextureMeshVisual),
+    'gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianSurfaceMeshParams>) => UnitsRepresentation('Gaussian surface mesh', ctx, getParams, GaussianSurfaceVisual),
+    'structure-gaussian-surface-mesh': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, StructureGaussianSurfaceMeshParams>) => ComplexRepresentation('Structure-Gaussian surface mesh', ctx, getParams, StructureGaussianSurfaceVisual),
     'gaussian-surface-wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, GaussianWireframeParams>) => UnitsRepresentation('Gaussian surface wireframe', ctx, getParams, GaussianWireframeVisual),
 };
 

+ 1 - 1
src/mol-repr/structure/representation/line.ts

@@ -23,7 +23,7 @@ const LineVisuals = {
 export const LineParams = {
     ...IntraUnitBondLineParams,
     ...InterUnitBondLineParams,
-    sizeFactor: PD.Numeric(1.0, { min: 0.01, max: 10, step: 0.01 }),
+    sizeFactor: PD.Numeric(1.5, { min: 0.01, max: 10, step: 0.01 }),
     unitKinds: getUnitKindsParam(['atomic']),
     visuals: PD.MultiSelect(['intra-bond', 'inter-bond'], PD.objectToOptions(LineVisuals))
 };

+ 3 - 3
src/mol-repr/structure/representation/spacefill.ts

@@ -1,10 +1,10 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { getElementSphereVisual, ElementSphereParams } from '../visual/element-sphere';
+import { ElementSphereVisual, ElementSphereParams } from '../visual/element-sphere';
 import { UnitsRepresentation } from '../units-representation';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../representation';
@@ -13,7 +13,7 @@ import { ThemeRegistryContext } from '../../../mol-theme/theme';
 import { Structure } from '../../../mol-model/structure';
 
 const SpacefillVisuals = {
-    'element-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Sphere mesh', ctx, getParams, getElementSphereVisual(ctx.webgl)),
+    'element-sphere': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, ElementSphereParams>) => UnitsRepresentation('Sphere mesh', ctx, getParams, ElementSphereVisual),
 };
 
 export const SpacefillParams = {

+ 47 - 31
src/mol-repr/structure/units-representation.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -24,11 +24,13 @@ import { Mat4, EPSILON } from '../../mol-math/linear-algebra';
 import { Interval } from '../../mol-data/int';
 import { StructureParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
+import { WebGLContext } from '../../mol-gl/webgl/context';
 
 export interface UnitsVisual<P extends StructureParams> extends Visual<StructureGroup, P> { }
 
-export function UnitsRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number) => UnitsVisual<P>): StructureRepresentation<P> {
+export function UnitsRepresentation<P extends StructureParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, P>, visualCtor: (materialId: number, props?: PD.Values<P>, webgl?: WebGLContext) => UnitsVisual<P>): StructureRepresentation<P> {
     let version = 0;
+    const { webgl } = ctx;
     const updated = new Subject<number>();
     const materialId = getNextMaterialId();
     const renderObjects: GraphicsRenderObject[] = [];
@@ -57,11 +59,10 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                 _groups = structure.unitSymmetryGroups;
                 for (let i = 0; i < _groups.length; i++) {
                     const group = _groups[i];
-                    const visual = visualCtor(materialId);
-                    const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure });
+                    const visual = visualCtor(materialId, _props, webgl);
+                    const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
                     if (promise) await promise;
-                    // ensure state is current for new visual
-                    setVisualState(visual, group, _state);
+                    setVisualState(visual, group, _state); // current state for new visual
                     visuals.set(group.hashCode, { visual, group });
                     if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length });
                 }
@@ -80,9 +81,17 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                         // console.log(label, 'found visualGroup to reuse');
                         // console.log('old', visualGroup.group)
                         // console.log('new', group)
-                        const { visual } = visualGroup;
-                        const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure });
-                        if (promise) await promise;
+                        let { visual } = visualGroup;
+                        if (visual.mustRecreate?.(_props, webgl)) {
+                            visual.destroy();
+                            visual = visualCtor(materialId, _props, webgl);
+                            const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
+                            if (promise) await promise;
+                            setVisualState(visual, group, _state); // current state for new visual
+                        } else {
+                            const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
+                            if (promise) await promise;
+                        }
                         visuals.set(group.hashCode, { visual, group });
                         oldVisuals.delete(group.hashCode);
 
@@ -95,11 +104,10 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                     } else {
                         // console.log(label, 'did not find visualGroup to reuse, creating new');
                         // newGroups.push(group)
-                        const visual = visualCtor(materialId);
-                        const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure });
+                        const visual = visualCtor(materialId, _props, webgl);
+                        const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
                         if (promise) await promise;
-                        // ensure state is current for new visual
-                        setVisualState(visual, group, _state);
+                        setVisualState(visual, group, _state); // current state for new visual
                         visuals.set(group.hashCode, { visual, group });
                     }
                     if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: _groups.length });
@@ -108,17 +116,6 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                     // console.log(label, 'removed unused visual');
                     visual.destroy();
                 });
-
-                // TODO review logic
-                // For new groups, re-use left-over visuals
-                // const unusedVisuals: UnitsVisual<P>[] = []
-                // oldVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
-                // newGroups.forEach(async group => {
-                //     const visual = unusedVisuals.pop() || visualCtor()
-                //     await visual.createOrUpdate({ ...ctx, runtime }, _props, group)
-                //     visuals.set(group.hashCode, { visual, group })
-                // })
-                // unusedVisuals.forEach(visual => visual.destroy())
             } else if (structure && structure !== _structure && Structure.areUnitIdsAndIndicesEqual(structure, _structure)) {
                 // console.log(label, 'structures equivalent but not identical');
                 // Expects that for structures with the same hashCode,
@@ -131,8 +128,18 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
                     const group = _groups[i];
                     const visualGroup = visuals.get(group.hashCode);
                     if (visualGroup) {
-                        const promise = visualGroup.visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, { group, structure });
-                        if (promise) await promise;
+                        let { visual } = visualGroup;
+                        if (visual.mustRecreate?.(_props, ctx.webgl)) {
+                            visual.destroy();
+                            visual = visualCtor(materialId, _props, ctx.webgl);
+                            visualGroup.visual = visual;
+                            const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
+                            if (promise) await promise;
+                            setVisualState(visual, group, _state); // current state for new visual
+                        } else {
+                            const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure });
+                            if (promise) await promise;
+                        }
                         visualGroup.group = group;
                     } else {
                         throw new Error(`expected to find visual for hashCode ${group.hashCode}`);
@@ -142,12 +149,21 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
             } else {
                 // console.log(label, 'no new structure');
                 // No new structure given, just update all visuals with new props.
-                const visualsList: [ UnitsVisual<P>, Unit.SymmetryGroup ][] = []; // TODO avoid allocation
-                visuals.forEach(({ visual, group }) => visualsList.push([ visual, group ]));
+                const visualsList: { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }[] = []; // TODO avoid allocation
+                visuals.forEach(vg => visualsList.push(vg));
                 for (let i = 0, il = visualsList.length; i < il; ++i) {
-                    const [ visual ] = visualsList[i];
-                    const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props);
-                    if (promise) await promise;
+                    let { visual, group } = visualsList[i];
+                    if (visual.mustRecreate?.(_props, ctx.webgl)) {
+                        visual.destroy();
+                        visual = visualCtor(materialId, _props, webgl);
+                        visualsList[i].visual = visual;
+                        const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, { group, structure: _structure });
+                        if (promise) await promise;
+                        setVisualState(visual, group, _state); // current state for new visual
+                    } else {
+                        const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props);
+                        if (promise) await promise;
+                    }
                     if (runtime.shouldUpdate) await runtime.update({ message: 'Creating or updating UnitsVisual', current: i, max: il });
                 }
             }

+ 25 - 4
src/mol-repr/structure/units-visual.ts

@@ -29,13 +29,14 @@ import { Transparency } from '../../mol-theme/transparency';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { SizeTheme } from '../../mol-theme/size';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
+import { Cylinders } from '../../mol-geo/geometry/cylinders/cylinders';
 import { Points } from '../../mol-geo/geometry/points/points';
 import { Lines } from '../../mol-geo/geometry/lines/lines';
 import { Text } from '../../mol-geo/geometry/text/text';
 import { DirectVolume } from '../../mol-geo/geometry/direct-volume/direct-volume';
 import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 import { SizeValues } from '../../mol-gl/renderable/schema';
-import { StructureParams, StructureMeshParams, StructureSpheresParams, StructurePointsParams, StructureLinesParams, StructureTextParams, StructureDirectVolumeParams, StructureTextureMeshParams } from './params';
+import { StructureParams, StructureMeshParams, StructureSpheresParams, StructurePointsParams, StructureLinesParams, StructureTextParams, StructureDirectVolumeParams, StructureTextureMeshParams, StructureCylindersParams } from './params';
 import { Clipping } from '../../mol-theme/clipping';
 
 export type StructureGroup = { structure: Structure, group: Unit.SymmetryGroup }
@@ -57,6 +58,8 @@ interface UnitsVisualBuilder<P extends StructureParams, G extends Geometry> {
     getLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number): Loci
     eachLocation(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean, isMarking: boolean): boolean
     setUpdateState(state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup): void
+    mustRecreate?: (props: PD.Values<P>) => boolean
+    dispose?: (geometry: G) => void
 }
 
 interface UnitsVisualGeometryBuilder<P extends StructureParams, G extends Geometry> extends UnitsVisualBuilder<P, G> {
@@ -64,7 +67,7 @@ interface UnitsVisualGeometryBuilder<P extends StructureParams, G extends Geomet
 }
 
 export function UnitsVisual<G extends Geometry, P extends StructureParams & Geometry.Params<G>>(builder: UnitsVisualGeometryBuilder<P, G>, materialId: number): UnitsVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder;
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, dispose } = builder;
     const { createEmpty: createEmptyGeometry, updateValues, updateBoundingSphere, updateRenderableState, createPositionIterator } = builder.geometryUtils;
     const updateState = VisualUpdateState.create();
 
@@ -290,9 +293,10 @@ export function UnitsVisual<G extends Geometry, P extends StructureParams & Geom
             Visual.setClipping(renderObject, clipping, lociApply, true);
         },
         destroy() {
-            // TODO
+            dispose?.(geometry);
             renderObject = undefined;
-        }
+        },
+        mustRecreate
     };
 }
 
@@ -330,6 +334,23 @@ export function UnitsSpheresVisual<P extends UnitsSpheresParams>(builder: UnitsS
     }, materialId);
 }
 
+// cylinders
+
+export const UnitsCylindersParams = { ...StructureCylindersParams, ...StructureParams };
+export type UnitsCylindersParams = typeof UnitsCylindersParams
+export interface UnitsCylindersVisualBuilder<P extends UnitsCylindersParams> extends UnitsVisualBuilder<P, Cylinders> { }
+
+export function UnitsCylindersVisual<P extends UnitsCylindersParams>(builder: UnitsCylindersVisualBuilder<P>, materialId: number): UnitsVisual<P> {
+    return UnitsVisual<Cylinders, P>({
+        ...builder,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
+            builder.setUpdateState(state, newProps, currentProps, newTheme, currentTheme, newStructureGroup, currentStructureGroup);
+            if (!SizeTheme.areEqual(newTheme.size, currentTheme.size)) state.updateSize = true;
+        },
+        geometryUtils: Cylinders.Utils
+    }, materialId);
+}
+
 // points
 
 export const UnitsPointsParams = { ...StructurePointsParams, ...StructureParams };

+ 84 - 18
src/mol-repr/structure/visual/bond-inter-unit-cylinder.ts

@@ -11,12 +11,14 @@ import { Theme } from '../../../mol-theme/theme';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { BitFlags, arrayEqual } from '../../../mol-util';
-import { createLinkCylinderMesh, LinkStyle } from './util/link';
-import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual } from '../complex-visual';
+import { createLinkCylinderImpostors, createLinkCylinderMesh, LinkStyle } from './util/link';
+import { ComplexMeshParams, ComplexVisual, ComplexMeshVisual, ComplexCylindersParams, ComplexCylindersVisual } from '../complex-visual';
 import { VisualUpdateState } from '../../util';
 import { BondType } from '../../../mol-model/structure/model/types';
 import { BondCylinderParams, BondIterator, getInterBondLoci, eachInterBond, makeInterBondIgnoreTest } from './util/bond';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
+import { WebGLContext } from '../../../mol-gl/webgl/context';
 
 const tmpRefPosBondIt = new Bond.ElementBondIterator();
 function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, index: StructureElement.UnitIndex) {
@@ -30,34 +32,41 @@ function setRefPosition(pos: Vec3, structure: Structure, unit: Unit.Atomic, inde
 }
 
 const tmpRef = Vec3();
-const tmpLoc = StructureElement.Location.create(void 0);
 
-function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>, mesh?: Mesh) {
+function getInterUnitBondCylinderBuilderProps(structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>) {
+    const locE = StructureElement.Location.create(structure);
+    const locB = Bond.Location(structure, undefined, undefined, structure, undefined, undefined);
+
     const bonds = structure.interUnitBonds;
     const { edgeCount, edges } = bonds;
     const { sizeFactor, sizeAspectRatio } = props;
 
-    if (!edgeCount) return Mesh.createEmpty(mesh);
-
     const delta = Vec3();
 
+    const radius = (edgeIndex: number) => {
+        const b = edges[edgeIndex];
+        locB.aUnit = structure.unitMap.get(b.unitA);
+        locB.aIndex = b.indexA;
+        locB.bUnit = structure.unitMap.get(b.unitB);
+        locB.bIndex = b.indexB;
+        return theme.size.size(locB) * sizeFactor;
+    };
+
     const radiusA = (edgeIndex: number) => {
         const b = edges[edgeIndex];
-        tmpLoc.structure = structure;
-        tmpLoc.unit = structure.unitMap.get(b.unitA);
-        tmpLoc.element = tmpLoc.unit.elements[b.indexA];
-        return theme.size.size(tmpLoc) * sizeFactor;
+        locE.unit = structure.unitMap.get(b.unitA);
+        locE.element = locE.unit.elements[b.indexA];
+        return theme.size.size(locE) * sizeFactor;
     };
 
     const radiusB = (edgeIndex: number) => {
         const b = edges[edgeIndex];
-        tmpLoc.structure = structure;
-        tmpLoc.unit = structure.unitMap.get(b.unitB);
-        tmpLoc.element = tmpLoc.unit.elements[b.indexB];
-        return theme.size.size(tmpLoc) * sizeFactor;
+        locE.unit = structure.unitMap.get(b.unitB);
+        locE.element = locE.unit.elements[b.indexB];
+        return theme.size.size(locE) * sizeFactor;
     };
 
-    const builderProps = {
+    return {
         linkCount: edgeCount,
         referencePosition: (edgeIndex: number) => {
             const b = edges[edgeIndex];
@@ -112,14 +121,31 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
             }
         },
         radius: (edgeIndex: number) => {
-            return Math.min(radiusA(edgeIndex), radiusB(edgeIndex)) * sizeAspectRatio;
+            return radius(edgeIndex) * sizeAspectRatio;
         },
         ignore: makeInterBondIgnoreTest(structure, props)
     };
+}
+
+function createInterUnitBondCylinderImpostors(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>, cylinders?: Cylinders) {
+    if (!structure.interUnitBonds.edgeCount) return Cylinders.createEmpty(cylinders);
 
+    const builderProps = getInterUnitBondCylinderBuilderProps(structure, theme, props);
+    const m = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
+
+    const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * props.sizeFactor);
+    m.setBoundingSphere(sphere);
+
+    return m;
+}
+
+function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InterUnitBondCylinderParams>, mesh?: Mesh) {
+    if (!structure.interUnitBonds.edgeCount) return Mesh.createEmpty(mesh);
+
+    const builderProps = getInterUnitBondCylinderBuilderProps(structure, theme, props);
     const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
 
-    const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * sizeFactor);
+    const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, 1 * props.sizeFactor);
     m.setBoundingSphere(sphere);
 
     return m;
@@ -127,13 +153,47 @@ function createInterUnitBondCylinderMesh(ctx: VisualContext, structure: Structur
 
 export const InterUnitBondCylinderParams = {
     ...ComplexMeshParams,
+    ...ComplexCylindersParams,
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
+    useImpostor: PD.Boolean(true),
 };
 export type InterUnitBondCylinderParams = typeof InterUnitBondCylinderParams
 
-export function InterUnitBondCylinderVisual(materialId: number): ComplexVisual<InterUnitBondCylinderParams> {
+export function InterUnitBondCylinderVisual(materialId: number, props?: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) {
+    return props?.useImpostor && webgl && webgl.extensions.fragDepth
+        ? InterUnitBondCylinderImpostorVisual(materialId)
+        : InterUnitBondCylinderMeshVisual(materialId);
+}
+
+export function InterUnitBondCylinderImpostorVisual(materialId: number): ComplexVisual<InterUnitBondCylinderParams> {
+    return ComplexCylindersVisual<InterUnitBondCylinderParams>({
+        defaultProps: PD.getDefaultValues(InterUnitBondCylinderParams),
+        createGeometry: createInterUnitBondCylinderImpostors,
+        createLocationIterator: BondIterator.fromStructure,
+        getLoci: getInterBondLoci,
+        eachLocation: eachInterBond,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<InterUnitBondCylinderParams>, currentProps: PD.Values<InterUnitBondCylinderParams>) => {
+            state.createGeometry = (
+                newProps.linkScale !== currentProps.linkScale ||
+                newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
+                newProps.linkCap !== currentProps.linkCap ||
+                newProps.dashCount !== currentProps.dashCount ||
+                newProps.dashScale !== currentProps.dashScale ||
+                newProps.dashCap !== currentProps.dashCap ||
+                !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
+                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
+            );
+        },
+        mustRecreate: (props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return !props.useImpostor || !webgl;
+        }
+    }, materialId);
+}
+
+export function InterUnitBondCylinderMeshVisual(materialId: number): ComplexVisual<InterUnitBondCylinderParams> {
     return ComplexMeshVisual<InterUnitBondCylinderParams>({
         defaultProps: PD.getDefaultValues(InterUnitBondCylinderParams),
         createGeometry: createInterUnitBondCylinderMesh,
@@ -149,9 +209,15 @@ export function InterUnitBondCylinderVisual(materialId: number): ComplexVisual<I
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.linkCap !== currentProps.linkCap ||
+                newProps.dashCount !== currentProps.dashCount ||
+                newProps.dashScale !== currentProps.dashScale ||
+                newProps.dashCap !== currentProps.dashCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );
+        },
+        mustRecreate: (props: PD.Values<InterUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return props.useImpostor && !!webgl;
         }
     }, materialId);
 }

+ 1 - 0
src/mol-repr/structure/visual/bond-inter-unit-line.ts

@@ -121,6 +121,7 @@ export function InterUnitBondLineVisual(materialId: number): ComplexVisual<Inter
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.linkScale !== currentProps.linkScale ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.dashCount !== currentProps.dashCount ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)

+ 93 - 18
src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts

@@ -7,26 +7,27 @@
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { VisualContext } from '../../visual';
-import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
+import { Unit, Structure, StructureElement, Bond } from '../../../mol-model/structure';
 import { Theme } from '../../../mol-theme/theme';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { arrayEqual } from '../../../mol-util';
-import { createLinkCylinderMesh, LinkStyle } from './util/link';
-import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup } from '../units-visual';
+import { createLinkCylinderImpostors, createLinkCylinderMesh, LinkStyle } from './util/link';
+import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual, StructureGroup, UnitsCylindersParams, UnitsCylindersVisual } from '../units-visual';
 import { VisualUpdateState } from '../../util';
 import { BondType } from '../../../mol-model/structure/model/types';
 import { BondCylinderParams, BondIterator, eachIntraBond, getIntraBondLoci, makeIntraBondIgnoreTest } from './util/bond';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { IntAdjacencyGraph } from '../../../mol-math/graph';
+import { WebGLContext } from '../../../mol-gl/webgl/context';
+import { Cylinders } from '../../../mol-geo/geometry/cylinders/cylinders';
 
 // avoiding namespace lookup improved performance in Chrome (Aug 2020)
 const isBondType = BondType.is;
 
-function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>, mesh?: Mesh) {
-    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
-
-    const location = StructureElement.Location.create(structure, unit);
+function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>) {
+    const locE = StructureElement.Location.create(structure, unit);
+    const locB = Bond.Location(structure, unit, undefined, structure, unit, undefined);
 
     const elements = unit.elements;
     const bonds = unit.bonds;
@@ -34,22 +35,26 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
     const { order: _order, flags: _flags } = edgeProps;
     const { sizeFactor, sizeAspectRatio } = props;
 
-    if (!edgeCount) return Mesh.createEmpty(mesh);
-
     const vRef = Vec3(), delta = Vec3();
     const pos = unit.conformation.invariantPosition;
 
+    const radius = (edgeIndex: number) => {
+        locB.aIndex = a[edgeIndex];
+        locB.bIndex = b[edgeIndex];
+        return theme.size.size(locB) * sizeFactor;
+    };
+
     const radiusA = (edgeIndex: number) => {
-        location.element = elements[a[edgeIndex]];
-        return theme.size.size(location) * sizeFactor;
+        locE.element = elements[a[edgeIndex]];
+        return theme.size.size(locE) * sizeFactor;
     };
 
     const radiusB = (edgeIndex: number) => {
-        location.element = elements[b[edgeIndex]];
-        return theme.size.size(location) * sizeFactor;
+        locE.element = elements[b[edgeIndex]];
+        return theme.size.size(locE) * sizeFactor;
     };
 
-    const builderProps = {
+    return {
         linkCount: edgeCount * 2,
         referencePosition: (edgeIndex: number) => {
             let aI = a[edgeIndex], bI = b[edgeIndex];
@@ -98,14 +103,33 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
             }
         },
         radius: (edgeIndex: number) => {
-            return Math.min(radiusA(edgeIndex), radiusB(edgeIndex)) * sizeAspectRatio;
+            return radius(edgeIndex) * sizeAspectRatio;
         },
         ignore: makeIntraBondIgnoreTest(unit, props)
     };
+}
 
+function createIntraUnitBondCylinderImpostors(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>, cylinders?: Cylinders): Cylinders {
+    if (!Unit.isAtomic(unit)) return Cylinders.createEmpty(cylinders);
+    if (!unit.bonds.edgeCount) return Cylinders.createEmpty(cylinders);
+
+    const builderProps = getIntraUnitBondCylinderBuilderProps(unit, structure, theme, props);
+    const c = createLinkCylinderImpostors(ctx, builderProps, props, cylinders);
+
+    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
+    c.setBoundingSphere(sphere);
+
+    return c;
+}
+
+function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<IntraUnitBondCylinderParams>, mesh?: Mesh): Mesh {
+    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
+    if (!unit.bonds.edgeCount) return Mesh.createEmpty(mesh);
+
+    const builderProps = getIntraUnitBondCylinderBuilderProps(unit, structure, theme, props);
     const m = createLinkCylinderMesh(ctx, builderProps, props, mesh);
 
-    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * sizeFactor);
+    const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1 * props.sizeFactor);
     m.setBoundingSphere(sphere);
 
     return m;
@@ -113,13 +137,58 @@ function createIntraUnitBondCylinderMesh(ctx: VisualContext, unit: Unit, structu
 
 export const IntraUnitBondCylinderParams = {
     ...UnitsMeshParams,
+    ...UnitsCylindersParams,
     ...BondCylinderParams,
     sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
     sizeAspectRatio: PD.Numeric(2 / 3, { min: 0, max: 3, step: 0.01 }),
+    useImpostor: PD.Boolean(true),
 };
 export type IntraUnitBondCylinderParams = typeof IntraUnitBondCylinderParams
 
-export function IntraUnitBondCylinderVisual(materialId: number): UnitsVisual<IntraUnitBondCylinderParams> {
+export function IntraUnitBondCylinderVisual(materialId: number, props?: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) {
+    return props?.useImpostor && webgl && webgl.extensions.fragDepth
+        ? IntraUnitBondCylinderImpostorVisual(materialId)
+        : IntraUnitBondCylinderMeshVisual(materialId);
+}
+
+export function IntraUnitBondCylinderImpostorVisual(materialId: number): UnitsVisual<IntraUnitBondCylinderParams> {
+    return UnitsCylindersVisual<IntraUnitBondCylinderParams>({
+        defaultProps: PD.getDefaultValues(IntraUnitBondCylinderParams),
+        createGeometry: createIntraUnitBondCylinderImpostors,
+        createLocationIterator: BondIterator.fromGroup,
+        getLoci: getIntraBondLoci,
+        eachLocation: eachIntraBond,
+        setUpdateState: (state: VisualUpdateState, newProps: PD.Values<IntraUnitBondCylinderParams>, currentProps: PD.Values<IntraUnitBondCylinderParams>, newTheme: Theme, currentTheme: Theme, newStructureGroup: StructureGroup, currentStructureGroup: StructureGroup) => {
+            state.createGeometry = (
+                newProps.linkScale !== currentProps.linkScale ||
+                newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
+                newProps.linkCap !== currentProps.linkCap ||
+                newProps.dashCount !== currentProps.dashCount ||
+                newProps.dashScale !== currentProps.dashScale ||
+                newProps.dashCap !== currentProps.dashCap ||
+                !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
+                !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
+            );
+
+            const newUnit = newStructureGroup.group.units[0];
+            const currentUnit = currentStructureGroup.group.units[0];
+            if (Unit.isAtomic(newUnit) && Unit.isAtomic(currentUnit)) {
+                if (!IntAdjacencyGraph.areEqual(newUnit.bonds, currentUnit.bonds)) {
+                    state.createGeometry = true;
+                    state.updateTransform = true;
+                    state.updateColor = true;
+                    state.updateSize = true;
+                }
+            }
+        },
+        mustRecreate: (props: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return !props.useImpostor || !webgl;
+        }
+    }, materialId);
+}
+
+export function IntraUnitBondCylinderMeshVisual(materialId: number): UnitsVisual<IntraUnitBondCylinderParams> {
     return UnitsMeshVisual<IntraUnitBondCylinderParams>({
         defaultProps: PD.getDefaultValues(IntraUnitBondCylinderParams),
         createGeometry: createIntraUnitBondCylinderMesh,
@@ -135,6 +204,9 @@ export function IntraUnitBondCylinderVisual(materialId: number): UnitsVisual<Int
                 newProps.linkSpacing !== currentProps.linkSpacing ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.linkCap !== currentProps.linkCap ||
+                newProps.dashCount !== currentProps.dashCount ||
+                newProps.dashScale !== currentProps.dashScale ||
+                newProps.dashCap !== currentProps.dashCap ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)
             );
@@ -149,6 +221,9 @@ export function IntraUnitBondCylinderVisual(materialId: number): UnitsVisual<Int
                     state.updateSize = true;
                 }
             }
+        },
+        mustRecreate: (props: PD.Values<IntraUnitBondCylinderParams>, webgl?: WebGLContext) => {
+            return props.useImpostor && !!webgl;
         }
     }, materialId);
-}
+}

+ 1 - 0
src/mol-repr/structure/visual/bond-intra-unit-line.ts

@@ -111,6 +111,7 @@ export function IntraUnitBondLineVisual(materialId: number): UnitsVisual<IntraUn
                 newProps.sizeFactor !== currentProps.sizeFactor ||
                 newProps.linkScale !== currentProps.linkScale ||
                 newProps.linkSpacing !== currentProps.linkSpacing ||
+                newProps.dashCount !== currentProps.dashCount ||
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 !arrayEqual(newProps.includeTypes, currentProps.includeTypes) ||
                 !arrayEqual(newProps.excludeTypes, currentProps.excludeTypes)

+ 12 - 3
src/mol-repr/structure/visual/element-sphere.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -19,11 +19,14 @@ export const ElementSphereParams = {
     detail: PD.Numeric(0, { min: 0, max: 3, step: 1 }, BaseGeometry.CustomQualityParamInfo),
     ignoreHydrogens: PD.Boolean(false),
     traceOnly: PD.Boolean(false),
+    useImpostor: PD.Boolean(true),
 };
 export type ElementSphereParams = typeof ElementSphereParams
 
-export function getElementSphereVisual(webgl?: WebGLContext) {
-    return webgl && webgl.extensions.fragDepth ? ElementSphereImpostorVisual : ElementSphereMeshVisual;
+export function ElementSphereVisual(materialId: number, props?: PD.Values<ElementSphereParams>, webgl?: WebGLContext) {
+    return props?.useImpostor && webgl && webgl.extensions.fragDepth
+        ? ElementSphereImpostorVisual(materialId)
+        : ElementSphereMeshVisual(materialId);
 }
 
 export function ElementSphereImpostorVisual(materialId: number): UnitsVisual<ElementSphereParams> {
@@ -38,6 +41,9 @@ export function ElementSphereImpostorVisual(materialId: number): UnitsVisual<Ele
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.traceOnly !== currentProps.traceOnly
             );
+        },
+        mustRecreate: (props: PD.Values<ElementSphereParams>, webgl?: WebGLContext) => {
+            return !props.useImpostor || !webgl;
         }
     }, materialId);
 }
@@ -56,6 +62,9 @@ export function ElementSphereMeshVisual(materialId: number): UnitsVisual<Element
                 newProps.ignoreHydrogens !== currentProps.ignoreHydrogens ||
                 newProps.traceOnly !== currentProps.traceOnly
             );
+        },
+        mustRecreate: (props: PD.Values<ElementSphereParams>, webgl?: WebGLContext) => {
+            return props.useImpostor && !!webgl;
         }
     }, materialId);
 }

+ 12 - 6
src/mol-repr/structure/visual/gaussian-density-volume.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -8,7 +8,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { VisualContext } from '../../visual';
 import { Structure, Unit } from '../../../mol-model/structure';
 import { Theme } from '../../../mol-theme/theme';
-import { GaussianDensityTextureProps, computeStructureGaussianDensityTexture, computeUnitGaussianDensityTexture, GaussianDensityTextureParams } from './util/gaussian';
+import { GaussianDensityProps, computeStructureGaussianDensityTexture, computeUnitGaussianDensityTexture, GaussianDensityParams } from './util/gaussian';
 import { DirectVolume } from '../../../mol-geo/geometry/direct-volume/direct-volume';
 import { ComplexDirectVolumeParams, ComplexVisual, ComplexDirectVolumeVisual } from '../complex-visual';
 import { VisualUpdateState } from '../../util';
@@ -18,7 +18,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
 import { UnitsDirectVolumeParams, UnitsVisual, UnitsDirectVolumeVisual } from '../units-visual';
 import { getStructureExtraRadius, getUnitExtraRadius } from './util/common';
 
-async function createGaussianDensityVolume(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, directVolume?: DirectVolume): Promise<DirectVolume> {
+async function createGaussianDensityVolume(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityProps, directVolume?: DirectVolume): Promise<DirectVolume> {
     const { runtime, webgl } = ctx;
     if (webgl === undefined) throw new Error('createGaussianDensityVolume requires `webgl` object in VisualContext');
 
@@ -41,7 +41,7 @@ async function createGaussianDensityVolume(ctx: VisualContext, structure: Struct
 
 export const GaussianDensityVolumeParams = {
     ...ComplexDirectVolumeParams,
-    ...GaussianDensityTextureParams,
+    ...GaussianDensityParams,
     ignoreHydrogens: PD.Boolean(false),
 };
 export type GaussianDensityVolumeParams = typeof GaussianDensityVolumeParams
@@ -60,13 +60,16 @@ export function GaussianDensityVolumeVisual(materialId: number): ComplexVisual<G
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+        },
+        dispose: (geometry: DirectVolume) => {
+            geometry.gridTexture.ref.value.destroy();
         }
     }, materialId);
 }
 
 //
 
-async function createUnitsGaussianDensityVolume(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, directVolume?: DirectVolume): Promise<DirectVolume> {
+async function createUnitsGaussianDensityVolume(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, directVolume?: DirectVolume): Promise<DirectVolume> {
     const { runtime, webgl } = ctx;
     if (webgl === undefined) throw new Error('createUnitGaussianDensityVolume requires `webgl` object in VisualContext');
 
@@ -89,7 +92,7 @@ async function createUnitsGaussianDensityVolume(ctx: VisualContext, unit: Unit,
 
 export const UnitsGaussianDensityVolumeParams = {
     ...UnitsDirectVolumeParams,
-    ...GaussianDensityTextureParams,
+    ...GaussianDensityParams,
     ignoreHydrogens: PD.Boolean(false),
 };
 export type UnitsGaussianDensityVolumeParams = typeof UnitsGaussianDensityVolumeParams
@@ -108,6 +111,9 @@ export function UnitsGaussianDensityVolumeVisual(materialId: number): UnitsVisua
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+        },
+        dispose: (geometry: DirectVolume) => {
+            geometry.gridTexture.ref.value.destroy();
         }
     }, materialId);
 }

+ 55 - 14
src/mol-repr/structure/visual/gaussian-surface-mesh.ts

@@ -6,7 +6,7 @@
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { UnitsMeshParams, UnitsTextureMeshParams, UnitsVisual, UnitsMeshVisual, UnitsTextureMeshVisual } from '../units-visual';
-import { GaussianDensityParams, computeUnitGaussianDensity, GaussianDensityTextureProps, computeUnitGaussianDensityTexture2d, GaussianDensityProps, computeStructureGaussianDensity, computeStructureGaussianDensityTexture2d } from './util/gaussian';
+import { GaussianDensityParams, computeUnitGaussianDensity, computeUnitGaussianDensityTexture2d, GaussianDensityProps, computeStructureGaussianDensity, computeStructureGaussianDensityTexture2d } from './util/gaussian';
 import { VisualContext } from '../../visual';
 import { Unit, Structure } from '../../../mol-model/structure';
 import { Theme } from '../../../mol-theme/theme';
@@ -19,16 +19,46 @@ import { calcActiveVoxels } from '../../../mol-gl/compute/marching-cubes/active-
 import { createHistogramPyramid } from '../../../mol-gl/compute/histogram-pyramid/reduction';
 import { createIsosurfaceBuffers } from '../../../mol-gl/compute/marching-cubes/isosurface';
 import { Sphere3D } from '../../../mol-math/geometry';
-import { ComplexVisual, ComplexMeshParams, ComplexMeshVisual, ComplexTextureMeshVisual } from '../complex-visual';
+import { ComplexVisual, ComplexMeshParams, ComplexMeshVisual, ComplexTextureMeshVisual, ComplexTextureMeshParams } from '../complex-visual';
 import { getUnitExtraRadius, getStructureExtraRadius } from './util/common';
+import { WebGLContext } from '../../../mol-gl/webgl/context';
+
+const SharedParams = {
+    ...GaussianDensityParams,
+    ignoreHydrogens: PD.Boolean(false),
+    useGpu: PD.Boolean(false),
+};
 
 export const GaussianSurfaceMeshParams = {
     ...UnitsMeshParams,
     ...UnitsTextureMeshParams,
-    ...GaussianDensityParams,
+    ...SharedParams,
 };
 export type GaussianSurfaceMeshParams = typeof GaussianSurfaceMeshParams
 
+export const StructureGaussianSurfaceMeshParams = {
+    ...ComplexMeshParams,
+    ...ComplexTextureMeshParams,
+    ...SharedParams,
+};
+export type StructureGaussianSurfaceMeshParams = typeof StructureGaussianSurfaceMeshParams
+
+function gpuSupport(webgl: WebGLContext) {
+    return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.blendMinMax && webgl.extensions.drawBuffers;
+}
+
+export function GaussianSurfaceVisual(materialId: number, props?: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) {
+    return props?.useGpu && webgl && gpuSupport(webgl)
+        ? GaussianSurfaceTextureMeshVisual(materialId)
+        : GaussianSurfaceMeshVisual(materialId);
+}
+
+export function StructureGaussianSurfaceVisual(materialId: number, props?: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) {
+    return props?.useGpu && webgl && gpuSupport(webgl)
+        ? StructureGaussianSurfaceTextureMeshVisual(materialId)
+        : StructureGaussianSurfaceMeshVisual(materialId);
+}
+
 //
 
 async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, mesh?: Mesh): Promise<Mesh> {
@@ -62,23 +92,18 @@ export function GaussianSurfaceMeshVisual(materialId: number): UnitsVisual<Gauss
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true;
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true;
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true;
-            if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true;
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+        },
+        mustRecreate: (props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return props.useGpu && !!webgl;
         }
     }, materialId);
 }
 
 //
 
-export const StructureGaussianSurfaceMeshParams = {
-    ...ComplexMeshParams,
-    ...GaussianDensityParams,
-    ignoreHydrogens: PD.Boolean(false),
-};
-export type StructureGaussianSurfaceMeshParams = typeof StructureGaussianSurfaceMeshParams
-
 async function createStructureGaussianSurfaceMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityProps, mesh?: Mesh): Promise<Mesh> {
     const { smoothness } = props;
     const { transform, field, idField, radiusFactor } = await computeStructureGaussianDensity(structure, props, ctx.webgl).runInContext(ctx.runtime);
@@ -110,9 +135,11 @@ export function StructureGaussianSurfaceMeshVisual(materialId: number): ComplexV
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true;
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true;
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true;
-            if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true;
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
+        },
+        mustRecreate: (props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return props.useGpu && !!webgl;
         }
     }, materialId);
 }
@@ -121,7 +148,7 @@ export function StructureGaussianSurfaceMeshVisual(materialId: number): ComplexV
 
 const GaussianSurfaceName = 'gaussian-surface';
 
-async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
+async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: GaussianDensityProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
     if (!ctx.webgl) throw new Error('webgl context required to create gaussian surface texture-mesh');
 
     const { namedTextures, resources, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = ctx.webgl;
@@ -182,13 +209,20 @@ export function GaussianSurfaceTextureMeshVisual(materialId: number): UnitsVisua
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;
+        },
+        mustRecreate: (props: PD.Values<GaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return !props.useGpu || !webgl;
+        },
+        dispose: (geometry: TextureMesh) => {
+            geometry.normalTexture.ref.value.destroy();
+            geometry.vertexGroupTexture.ref.value.destroy();
         }
     }, materialId);
 }
 
 //
 
-async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityTextureProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
+async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: GaussianDensityProps, textureMesh?: TextureMesh): Promise<TextureMesh> {
     if (!ctx.webgl) throw new Error('webgl context required to create structure gaussian surface texture-mesh');
 
     const { namedTextures, resources, extensions: { colorBufferFloat, textureFloat, colorBufferHalfFloat, textureHalfFloat } } = ctx.webgl;
@@ -248,6 +282,13 @@ export function StructureGaussianSurfaceTextureMeshVisual(materialId: number): C
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true;
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
+        },
+        mustRecreate: (props: PD.Values<StructureGaussianSurfaceMeshParams>, webgl?: WebGLContext) => {
+            return !props.useGpu || !webgl;
+        },
+        dispose: (geometry: TextureMesh) => {
+            geometry.normalTexture.ref.value.destroy();
+            geometry.vertexGroupTexture.ref.value.destroy();
         }
     }, materialId);
 }

+ 2 - 3
src/mol-repr/structure/visual/gaussian-surface-wireframe.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -39,7 +39,7 @@ async function createGaussianWireframe(ctx: VisualContext, unit: Unit, structure
 export const GaussianWireframeParams = {
     ...UnitsLinesParams,
     ...GaussianDensityParams,
-    sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
+    sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
     lineSizeAttenuation: PD.Boolean(false),
     ignoreHydrogens: PD.Boolean(false),
 };
@@ -56,7 +56,6 @@ export function GaussianWireframeVisual(materialId: number): UnitsVisual<Gaussia
             if (newProps.resolution !== currentProps.resolution) state.createGeometry = true;
             if (newProps.radiusOffset !== currentProps.radiusOffset) state.createGeometry = true;
             if (newProps.smoothness !== currentProps.smoothness) state.createGeometry = true;
-            if (newProps.useGpu !== currentProps.useGpu) state.createGeometry = true;
             if (newProps.ignoreHydrogens !== currentProps.ignoreHydrogens) state.createGeometry = true;
             if (newProps.traceOnly !== currentProps.traceOnly) state.createGeometry = true;
             if (newProps.includeParent !== currentProps.includeParent) state.createGeometry = true;

+ 15 - 7
src/mol-repr/structure/visual/util/bond.ts

@@ -98,11 +98,15 @@ export namespace BondIterator {
         const unit = group.units[0];
         const groupCount = Unit.isAtomic(unit) ? unit.bonds.edgeCount * 2 : 0;
         const instanceCount = group.units.length;
-        const location = StructureElement.Location.create(structure);
+        const location = Bond.Location();
+        location.aStructure = structure;
+        location.bStructure = structure;
         const getLocation = (groupIndex: number, instanceIndex: number) => {
-            const unit = group.units[instanceIndex];
-            location.unit = unit;
-            location.element = unit.elements[(unit as Unit.Atomic).bonds.a[groupIndex]];
+            const unit = group.units[instanceIndex] as Unit.Atomic;
+            location.aUnit = unit;
+            location.bUnit = unit;
+            location.aIndex = unit.bonds.a[groupIndex];
+            location.bIndex = unit.bonds.b[groupIndex];
             return location;
         };
         return LocationIterator(groupCount, instanceCount, 1, getLocation);
@@ -111,11 +115,15 @@ export namespace BondIterator {
     export function fromStructure(structure: Structure): LocationIterator {
         const groupCount = structure.interUnitBonds.edgeCount;
         const instanceCount = 1;
-        const location = StructureElement.Location.create(structure);
+        const location = Bond.Location();
+        location.aStructure = structure;
+        location.bStructure = structure;
         const getLocation = (groupIndex: number) => {
             const bond = structure.interUnitBonds.edges[groupIndex];
-            location.unit = structure.unitMap.get(bond.unitA);
-            location.element = location.unit.elements[bond.indexA];
+            location.aUnit = structure.unitMap.get(bond.unitA);
+            location.aIndex = bond.indexA;
+            location.bUnit = structure.unitMap.get(bond.unitB);
+            location.bIndex = bond.indexB;
             return location;
         };
         return LocationIterator(groupCount, instanceCount, 1, getLocation, true);

+ 4 - 4
src/mol-repr/structure/visual/util/common.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -100,13 +100,13 @@ export function includesUnitKind(unitKinds: UnitKind[], unit: Unit) {
 
 //
 
-const MaxCells = 500_000_000;
+const DefaultMaxCells = 500_000_000;
 
 /** guard against overly high resolution for the given box size */
-export function ensureReasonableResolution<T>(box: Box3D, props: { resolution: number } & T) {
+export function ensureReasonableResolution<T>(box: Box3D, props: { resolution: number } & T, maxCells = DefaultMaxCells) {
     const volume = Box3D.volume(box);
     const approxCells = volume / props.resolution;
-    const resolution = approxCells > MaxCells ? volume / MaxCells : props.resolution;
+    const resolution = approxCells > maxCells ? volume / maxCells : props.resolution;
     return { ...props, resolution };
 }
 

+ 17 - 23
src/mol-repr/structure/visual/util/gaussian.ts

@@ -1,11 +1,10 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Unit, Structure } from '../../../../mol-model/structure';
-import { GaussianDensity } from '../../../../mol-math/geometry/gaussian-density';
 import { Task } from '../../../../mol-task';
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
 import { GaussianDensityTexture, GaussianDensityTexture2d } from '../../../../mol-math/geometry/gaussian-density/gpu';
@@ -13,26 +12,21 @@ import { Texture } from '../../../../mol-gl/webgl/texture';
 import { WebGLContext } from '../../../../mol-gl/webgl/context';
 import { getUnitConformationAndRadius, getStructureConformationAndRadius, CommonSurfaceParams, ensureReasonableResolution } from './common';
 import { BaseGeometry } from '../../../../mol-geo/geometry/base';
+import { GaussianDensityCPU } from '../../../../mol-math/geometry/gaussian-density/cpu';
 
-const SharedGaussianDensityParams = {
+export const GaussianDensityParams = {
     resolution: PD.Numeric(1, { min: 0.1, max: 20, step: 0.1 }, { description: 'Grid resolution/cell spacing.', ...BaseGeometry.CustomQualityParamInfo }),
     radiusOffset: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, { description: 'Extra/offset radius added to the atoms/coarse elements for gaussian calculation. Useful to create coarse, low resolution surfaces.' }),
     smoothness: PD.Numeric(1.5, { min: 0.5, max: 2.5, step: 0.1 }, { description: 'Smoothness of the gausian surface, lower is smoother.' }),
     ...CommonSurfaceParams
 };
-
-export const GaussianDensityParams = {
-    ...SharedGaussianDensityParams,
-    useGpu: PD.Boolean(false),
-};
 export const DefaultGaussianDensityProps = PD.getDefaultValues(GaussianDensityParams);
 export type GaussianDensityProps = typeof DefaultGaussianDensityProps
 
-export const GaussianDensityTextureParams = {
-    ...SharedGaussianDensityParams
-};
-export const DefaultGaussianDensityTextureProps = PD.getDefaultValues(GaussianDensityTextureParams);
-export type GaussianDensityTextureProps = typeof DefaultGaussianDensityTextureProps
+function getTextureMaxCells(webgl: WebGLContext) {
+    const d = Math.min(webgl.maxTextureSize / 2, 4096);
+    return d * d;
+}
 
 //
 
@@ -41,22 +35,22 @@ export function computeUnitGaussianDensity(structure: Structure, unit: Unit, pro
     const p = ensureReasonableResolution(box, props);
     const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
     return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensity(ctx, position, box, radius, p, webgl);
+        return await GaussianDensityCPU(ctx, position, box, radius, p);
     });
 }
 
-export function computeUnitGaussianDensityTexture(structure: Structure, unit: Unit, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+export function computeUnitGaussianDensityTexture(structure: Structure, unit: Unit, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     const { box } = unit.lookup3d.boundary;
-    const p = ensureReasonableResolution(box, props);
+    const p = ensureReasonableResolution(box, props, getTextureMaxCells(webgl));
     const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
     return Task.create('Gaussian Density', async ctx => {
         return GaussianDensityTexture(webgl, position, box, radius, p, texture);
     });
 }
 
-export function computeUnitGaussianDensityTexture2d(structure: Structure, unit: Unit, powerOfTwo: boolean, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+export function computeUnitGaussianDensityTexture2d(structure: Structure, unit: Unit, powerOfTwo: boolean, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     const { box } = unit.lookup3d.boundary;
-    const p = ensureReasonableResolution(box, props);
+    const p = ensureReasonableResolution(box, props, getTextureMaxCells(webgl));
     const { position, radius } = getUnitConformationAndRadius(structure, unit, p);
     return Task.create('Gaussian Density', async ctx => {
         return GaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, p, texture);
@@ -70,22 +64,22 @@ export function computeStructureGaussianDensity(structure: Structure, props: Gau
     const p = ensureReasonableResolution(box, props);
     const { position, radius } = getStructureConformationAndRadius(structure, props.ignoreHydrogens, props.traceOnly);
     return Task.create('Gaussian Density', async ctx => {
-        return await GaussianDensity(ctx, position, box, radius, p, webgl);
+        return await GaussianDensityCPU(ctx, position, box, radius, p);
     });
 }
 
-export function computeStructureGaussianDensityTexture(structure: Structure, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+export function computeStructureGaussianDensityTexture(structure: Structure, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     const { box } = structure.lookup3d.boundary;
-    const p = ensureReasonableResolution(box, props);
+    const p = ensureReasonableResolution(box, props, getTextureMaxCells(webgl));
     const { position, radius } = getStructureConformationAndRadius(structure, props.ignoreHydrogens, props.traceOnly);
     return Task.create('Gaussian Density', async ctx => {
         return GaussianDensityTexture(webgl, position, box, radius, p, texture);
     });
 }
 
-export function computeStructureGaussianDensityTexture2d(structure: Structure, powerOfTwo: boolean, props: GaussianDensityTextureProps, webgl: WebGLContext, texture?: Texture) {
+export function computeStructureGaussianDensityTexture2d(structure: Structure, powerOfTwo: boolean, props: GaussianDensityProps, webgl: WebGLContext, texture?: Texture) {
     const { box } = structure.lookup3d.boundary;
-    const p = ensureReasonableResolution(box, props);
+    const p = ensureReasonableResolution(box, props, getTextureMaxCells(webgl));
     const { position, radius } = getStructureConformationAndRadius(structure, props.ignoreHydrogens, props.traceOnly);
     return Task.create('Gaussian Density', async ctx => {
         return GaussianDensityTexture2d(webgl, position, box, radius, powerOfTwo, p, texture);

+ 93 - 18
src/mol-repr/structure/visual/util/link.ts

@@ -14,11 +14,16 @@ import { VisualContext } from '../../../visual';
 import { BaseGeometry } from '../../../../mol-geo/geometry/base';
 import { Lines } from '../../../../mol-geo/geometry/lines/lines';
 import { LinesBuilder } from '../../../../mol-geo/geometry/lines/lines-builder';
+import { Cylinders } from '../../../../mol-geo/geometry/cylinders/cylinders';
+import { CylindersBuilder } from '../../../../mol-geo/geometry/cylinders/cylinders-builder';
 
 export const LinkCylinderParams = {
     linkScale: PD.Numeric(0.4, { min: 0, max: 1, step: 0.1 }),
     linkSpacing: PD.Numeric(1, { min: 0, max: 2, step: 0.01 }),
     linkCap: PD.Boolean(false),
+    dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
+    dashScale: PD.Numeric(0.8, { min: 0, max: 2, step: 0.1 }),
+    dashCap: PD.Boolean(true),
     radialSegments: PD.Numeric(16, { min: 2, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
 };
 export const DefaultLinkCylinderProps = PD.getDefaultValues(LinkCylinderParams);
@@ -27,6 +32,7 @@ export type LinkCylinderProps = typeof DefaultLinkCylinderProps
 export const LinkLineParams = {
     linkScale: PD.Numeric(0.5, { min: 0, max: 1, step: 0.1 }),
     linkSpacing: PD.Numeric(0.1, { min: 0, max: 2, step: 0.01 }),
+    dashCount: PD.Numeric(4, { min: 2, max: 10, step: 2 }),
 };
 export const DefaultLinkLineProps = PD.getDefaultValues(LinkLineParams);
 export type LinkLineProps = typeof DefaultLinkLineProps
@@ -95,7 +101,7 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
 
     if (!linkCount) return Mesh.createEmpty(mesh);
 
-    const { linkScale, linkSpacing, radialSegments, linkCap } = props;
+    const { linkScale, linkSpacing, radialSegments, linkCap, dashCount, dashScale, dashCap } = props;
 
     const vertexCountEstimate = radialSegments * 2 * linkCount * 2;
     const builderState = MeshBuilder.createState(vertexCountEstimate, vertexCountEstimate / 4, mesh);
@@ -111,25 +117,30 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
         bottomCap: linkCap
     };
 
+    const segmentCount = dashCount + 1;
+
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
 
         position(va, vb, edgeIndex);
+        v3sub(tmpV12, vb, va);
 
         const linkRadius = radius(edgeIndex);
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
+        const [topCap, bottomCap] = (v3dot(tmpV12, up) > 0) ? [false, linkCap] : [linkCap, false];
         builderState.currentGroup = edgeIndex;
 
         if (linkStyle === LinkStyle.Solid) {
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
-            cylinderProps.topCap = cylinderProps.bottomCap = linkCap;
+            cylinderProps.topCap = topCap;
+            cylinderProps.bottomCap = bottomCap;
 
             addCylinder(builderState, va, vb, 0.5, cylinderProps);
         } else if (linkStyle === LinkStyle.Dashed) {
-            cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius / 3;
-            cylinderProps.topCap = cylinderProps.bottomCap = true;
+            cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius * dashScale;
+            cylinderProps.topCap = cylinderProps.bottomCap = dashCap;
 
-            addFixedCountDashedCylinder(builderState, va, vb, 0.5, 7, cylinderProps);
+            addFixedCountDashedCylinder(builderState, va, vb, 0.5, segmentCount, cylinderProps);
         } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
             const order = linkStyle === LinkStyle.Double ? 2 : 3;
             const multiRadius = linkRadius * (linkScale / (0.5 * order));
@@ -139,23 +150,19 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
             v3setMagnitude(vShift, vShift, absOffset);
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = multiRadius;
-            cylinderProps.topCap = cylinderProps.bottomCap = linkCap;
+            cylinderProps.topCap = topCap;
+            cylinderProps.bottomCap = bottomCap;
 
             if (order === 3) addCylinder(builderState, va, vb, 0.5, cylinderProps);
             addDoubleCylinder(builderState, va, vb, 0.5, vShift, cylinderProps);
         } else if (linkStyle === LinkStyle.Disk) {
-            v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
+            v3scale(tmpV12, tmpV12, 0.475);
             v3add(va, va, tmpV12);
             v3sub(vb, vb, tmpV12);
 
             cylinderProps.radiusTop = cylinderProps.radiusBottom = linkRadius;
-            if (v3dot(tmpV12, up) > 0) {
-                cylinderProps.topCap = false;
-                cylinderProps.bottomCap = linkCap;
-            } else {
-                cylinderProps.topCap = linkCap;
-                cylinderProps.bottomCap = false;
-            }
+            cylinderProps.topCap = topCap;
+            cylinderProps.bottomCap = bottomCap;
 
             addCylinder(builderState, va, vb, 0.5, cylinderProps);
         }
@@ -164,6 +171,67 @@ export function createLinkCylinderMesh(ctx: VisualContext, linkBuilder: LinkBuil
     return MeshBuilder.getMesh(builderState);
 }
 
+/**
+ * Each edge is included twice to allow for coloring/picking
+ * the half closer to the first vertex, i.e. vertex a.
+ */
+export function createLinkCylinderImpostors(ctx: VisualContext, linkBuilder: LinkBuilderProps, props: LinkCylinderProps, cylinders?: Cylinders) {
+    const { linkCount, referencePosition, position, style, radius, ignore } = linkBuilder;
+
+    if (!linkCount) return Cylinders.createEmpty(cylinders);
+
+    const { linkScale, linkSpacing, linkCap, dashCount, dashScale, dashCap } = props;
+
+    const cylindersCountEstimate = linkCount * 2;
+    const builder = CylindersBuilder.create(cylindersCountEstimate, cylindersCountEstimate / 4, cylinders);
+
+    const va = Vec3();
+    const vb = Vec3();
+    const vShift = Vec3();
+
+    // automatically adjust length for evenly spaced dashed cylinders
+    const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
+    const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
+
+    for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
+        if (ignore && ignore(edgeIndex)) continue;
+
+        position(va, vb, edgeIndex);
+
+        const linkRadius = radius(edgeIndex);
+        const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
+
+        if (linkStyle === LinkStyle.Solid) {
+            v3scale(vb, v3add(vb, va, vb), 0.5);
+            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, false, edgeIndex);
+        } else if (linkStyle === LinkStyle.Dashed) {
+            v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
+            v3sub(vb, vb, tmpV12);
+            builder.addFixedCountDashes(va, vb, segmentCount, dashScale, dashCap, dashCap, edgeIndex);
+        } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
+            v3scale(vb, v3add(vb, va, vb), 0.5);
+            const order = linkStyle === LinkStyle.Double ? 2 : 3;
+            const multiScale = linkScale / (0.5 * order);
+            const absOffset = (linkRadius - multiScale * linkRadius) * linkSpacing;
+
+            calculateShiftDir(vShift, va, vb, referencePosition ? referencePosition(edgeIndex) : null);
+            v3setMagnitude(vShift, vShift, absOffset);
+
+            if (order === 3) builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], multiScale, linkCap, false, edgeIndex);
+            builder.add(va[0] + vShift[0], va[1] + vShift[1], va[2] + vShift[2], vb[0] + vShift[0], vb[1] + vShift[1], vb[2] + vShift[2], multiScale, linkCap, false, edgeIndex);
+            builder.add(va[0] - vShift[0], va[1] - vShift[1], va[2] - vShift[2], vb[0] - vShift[0], vb[1] - vShift[1], vb[2] - vShift[2], multiScale, linkCap, false, edgeIndex);
+        } else if (linkStyle === LinkStyle.Disk) {
+            v3scale(tmpV12, v3sub(tmpV12, vb, va), 0.475);
+            v3add(va, va, tmpV12);
+            v3sub(vb, vb, tmpV12);
+
+            builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], 1, linkCap, false, edgeIndex);
+        }
+    }
+
+    return builder.getCylinders();
+}
+
 /**
  * Each edge is included twice to allow for coloring/picking
  * the half closer to the first vertex, i.e. vertex a.
@@ -173,7 +241,7 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
 
     if (!linkCount) return Lines.createEmpty(lines);
 
-    const { linkScale, linkSpacing } = props;
+    const { linkScale, linkSpacing, dashCount } = props;
 
     const linesCountEstimate = linkCount * 2;
     const builder = LinesBuilder.create(linesCountEstimate, linesCountEstimate / 4, lines);
@@ -182,20 +250,27 @@ export function createLinkLines(ctx: VisualContext, linkBuilder: LinkBuilderProp
     const vb = Vec3();
     const vShift = Vec3();
 
+    // automatically adjust length for evenly spaced dashed lines
+    const segmentCount = dashCount % 2 === 1 ? dashCount : dashCount + 1;
+    const lengthScale = 0.5 - (0.5 / 2 / segmentCount);
+
     for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
         if (ignore && ignore(edgeIndex)) continue;
 
         position(va, vb, edgeIndex);
-        v3scale(vb, v3add(vb, va, vb), 0.5);
 
         const linkStyle = style ? style(edgeIndex) : LinkStyle.Solid;
 
         if (linkStyle === LinkStyle.Solid) {
+            v3scale(vb, v3add(vb, va, vb), 0.5);
             builder.add(va[0], va[1], va[2], vb[0], vb[1], vb[2], edgeIndex);
         } else if (linkStyle === LinkStyle.Dashed) {
-            builder.addFixedCountDashes(va, vb, 7, edgeIndex);
+            v3scale(tmpV12, v3sub(tmpV12, vb, va), lengthScale);
+            v3sub(vb, vb, tmpV12);
+            builder.addFixedCountDashes(va, vb, segmentCount, edgeIndex);
         } else if (linkStyle === LinkStyle.Double || linkStyle === LinkStyle.Triple) {
-            const order = LinkStyle.Double ? 2 : 3;
+            v3scale(vb, v3add(vb, va, vb), 0.5);
+            const order = linkStyle === LinkStyle.Double ? 2 : 3;
             const multiRadius = 1 * (linkScale / (0.5 * order));
             const absOffset = (1 - multiRadius) * linkSpacing;
 

+ 1 - 0
src/mol-repr/visual.ts

@@ -47,6 +47,7 @@ interface Visual<D, P extends PD.Params> {
     setTransparency: (transparency: Transparency) => void
     setClipping: (clipping: Clipping) => void
     destroy: () => void
+    mustRecreate?: (props: PD.Values<P>, webgl?: WebGLContext) => boolean
 }
 namespace Visual {
     export type LociApply = (loci: Loci, apply: (interval: Interval) => boolean, isMarking: boolean) => boolean

+ 4 - 3
src/mol-repr/volume/direct-volume.ts

@@ -39,7 +39,6 @@ export function createDirectVolume2d(ctx: RuntimeContext, webgl: WebGLContext, v
     const transform = Grid.getGridToCartesianTransform(volume.grid);
     const bbox = getBoundingBox(gridDimension, transform);
 
-    // TODO: handle disposal
     const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
     texture.load(textureImage);
 
@@ -77,7 +76,6 @@ export function createDirectVolume3d(ctx: RuntimeContext, webgl: WebGLContext, v
     const transform = Grid.getGridToCartesianTransform(volume.grid);
     const bbox = getBoundingBox(gridDimension, transform);
 
-    // TODO: handle disposal
     const texture = directVolume ? directVolume.gridTexture.ref.value : webgl.resources.texture('volume-uint8', 'rgba', 'ubyte', 'linear');
     texture.load(textureVolume);
 
@@ -139,7 +137,10 @@ export function DirectVolumeVisual(materialId: number): VolumeVisual<DirectVolum
         eachLocation: eachDirectVolume,
         setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<DirectVolumeParams>, currentProps: PD.Values<DirectVolumeParams>) => {
         },
-        geometryUtils: DirectVolume.Utils
+        geometryUtils: DirectVolume.Utils,
+        dispose: (geometry: DirectVolume) => {
+            geometry.gridTexture.ref.value.destroy();
+        }
     }, materialId);
 }
 

+ 69 - 32
src/mol-repr/volume/isosurface.ts

@@ -27,6 +27,9 @@ import { TextureMesh } from '../../mol-geo/geometry/texture-mesh/texture-mesh';
 import { calcActiveVoxels } from '../../mol-gl/compute/marching-cubes/active-voxels';
 import { createHistogramPyramid } from '../../mol-gl/compute/histogram-pyramid/reduction';
 import { createIsosurfaceBuffers } from '../../mol-gl/compute/marching-cubes/isosurface';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
+import { Texture } from '../../mol-gl/webgl/texture';
 
 export const VolumeIsosurfaceParams = {
     isoValue: Volume.IsoValueParam
@@ -34,6 +37,16 @@ export const VolumeIsosurfaceParams = {
 export type VolumeIsosurfaceParams = typeof VolumeIsosurfaceParams
 export type VolumeIsosurfaceProps = PD.Values<VolumeIsosurfaceParams>
 
+function gpuSupport(webgl: WebGLContext) {
+    return webgl.extensions.colorBufferFloat && webgl.extensions.textureFloat && webgl.extensions.drawBuffers;
+}
+
+export function IsosurfaceVisual(materialId: number, props?: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) {
+    return props?.useGpu && webgl && gpuSupport(webgl)
+        ? IsosurfaceTextureMeshVisual(materialId)
+        : IsosurfaceMeshVisual(materialId);
+}
+
 function getLoci(volume: Volume, props: VolumeIsosurfaceProps) {
     return Volume.Isosurface.Loci(volume, props.isoValue);
 }
@@ -74,8 +87,10 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
 
 export const IsosurfaceMeshParams = {
     ...Mesh.Params,
+    ...TextureMesh.Params,
+    ...VolumeIsosurfaceParams,
     quality: { ...Mesh.Params.quality, isEssential: false },
-    ...VolumeIsosurfaceParams
+    useGpu: PD.Boolean(false),
 };
 export type IsosurfaceMeshParams = typeof IsosurfaceMeshParams
 
@@ -89,44 +104,61 @@ export function IsosurfaceMeshVisual(materialId: number): VolumeVisual<Isosurfac
         setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
             if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
         },
-        geometryUtils: Mesh.Utils
+        geometryUtils: Mesh.Utils,
+        mustRecreate: (props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
+            return props.useGpu && !!webgl;
+        }
     }, materialId);
 }
 
 //
 
-async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) {
-    if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
-
-    const { resources } = ctx.webgl;
-    if (!volume._propertyData['texture2d']) {
-        // TODO: handle disposal
-        volume._propertyData['texture2d'] = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+namespace VolumeIsosurfaceTexture {
+    const name = 'volume-isosurface-texture';
+    export const descriptor = CustomPropertyDescriptor({ name });
+    export function get(volume: Volume, webgl: WebGLContext) {
+        const { resources } = webgl;
+
+        const padding = 1;
+        const transform = Grid.getGridToCartesianTransform(volume.grid);
+        const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions as Vec3);
+        const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, padding);
+        const gridTexDim = Vec3.create(width, height, 0);
+        const gridTexScale = Vec2.create(width / texDim, height / texDim);
+        // console.log({ texDim, width, height, gridDimension });
+
+        if (!volume._propertyData[name]) {
+            volume._propertyData[name] = resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+            const texture = volume._propertyData[name] as Texture;
+            texture.define(texDim, texDim);
+            // load volume into sub-section of texture
+            texture.load(createVolumeTexture2d(volume, 'groups', padding), true);
+            volume.customProperties.add(descriptor);
+            volume.customProperties.assets(descriptor, [{ dispose: () => texture.destroy() }]);
+        }
+
+        gridDimension[0] += padding;
+        gridDimension[1] += padding;
+
+        return {
+            texture: volume._propertyData[name] as Texture,
+            transform,
+            gridDimension,
+            gridTexDim,
+            gridTexScale
+        };
     }
-    const texture = volume._propertyData['texture2d'];
+}
 
-    const padding = 1;
-    const transform = Grid.getGridToCartesianTransform(volume.grid);
-    const gridDimension = Vec3.clone(volume.grid.cells.space.dimensions as Vec3);
-    const { width, height, powerOfTwoSize: texDim } = getVolumeTexture2dLayout(gridDimension, padding);
-    const gridTexDim = Vec3.create(width, height, 0);
-    const gridTexScale = Vec2.create(width / texDim, height / texDim);
-    // console.log({ texDim, width, height, gridDimension });
-
-    if (!textureMesh) {
-        // set to power-of-two size required for histopyramid calculation
-        texture.define(texDim, texDim);
-        // load volume into sub-section of texture
-        texture.load(createVolumeTexture2d(volume, 'groups', padding), true);
-    }
+async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Volume, theme: Theme, props: VolumeIsosurfaceProps, textureMesh?: TextureMesh) {
+    if (!ctx.webgl) throw new Error('webgl context required to create volume isosurface texture-mesh');
 
     const { max, min } = volume.grid.stats;
     const diff = max - min;
     const value = Volume.IsoValue.toAbsolute(props.isoValue, volume.grid.stats).absoluteValue;
     const isoLevel = ((value - min) / diff);
 
-    gridDimension[0] += padding;
-    gridDimension[1] += padding;
+    const { texture, gridDimension, gridTexDim, gridTexScale, transform } = VolumeIsosurfaceTexture.get(volume, ctx.webgl);
 
     // console.time('calcActiveVoxels');
     const activeVoxelsTex = calcActiveVoxels(ctx.webgl, texture, gridDimension, gridTexDim, isoLevel, gridTexScale);
@@ -163,7 +195,14 @@ export function IsosurfaceTextureMeshVisual(materialId: number): VolumeVisual<Is
         setUpdateState: (state: VisualUpdateState, volume: Volume, newProps: PD.Values<IsosurfaceMeshParams>, currentProps: PD.Values<IsosurfaceMeshParams>) => {
             if (!Volume.IsoValue.areSame(newProps.isoValue, currentProps.isoValue, volume.grid.stats)) state.createGeometry = true;
         },
-        geometryUtils: TextureMesh.Utils
+        geometryUtils: TextureMesh.Utils,
+        mustRecreate: (props: PD.Values<IsosurfaceMeshParams>, webgl?: WebGLContext) => {
+            return !props.useGpu || !webgl;
+        },
+        dispose: (geometry: TextureMesh) => {
+            geometry.normalTexture.ref.value.destroy();
+            geometry.vertexGroupTexture.ref.value.destroy();
+        }
     }, materialId);
 }
 
@@ -190,9 +229,9 @@ export async function createVolumeIsosurfaceWireframe(ctx: VisualContext, volume
 
 export const IsosurfaceWireframeParams = {
     ...Lines.Params,
+    ...VolumeIsosurfaceParams,
     quality: { ...Lines.Params.quality, isEssential: false },
-    sizeFactor: PD.Numeric(1.5, { min: 0, max: 10, step: 0.1 }),
-    ...VolumeIsosurfaceParams
+    sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
 };
 export type IsosurfaceWireframeParams = typeof IsosurfaceWireframeParams
 
@@ -213,9 +252,7 @@ export function IsosurfaceWireframeVisual(materialId: number): VolumeVisual<Isos
 //
 
 const IsosurfaceVisuals = {
-    'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceMeshVisual, getLoci),
-    // TODO: don't enable yet as it breaks state sessions
-    // 'solid-gpu': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface texture-mesh', ctx, getParams, IsosurfaceTextureMeshVisual, getLoci),
+    'solid': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceMeshParams>) => VolumeRepresentation('Isosurface mesh', ctx, getParams, IsosurfaceVisual, getLoci),
     'wireframe': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, IsosurfaceWireframeParams>) => VolumeRepresentation('Isosurface wireframe', ctx, getParams, IsosurfaceWireframeVisual, getLoci),
 };
 

+ 16 - 6
src/mol-repr/volume/representation.ts

@@ -30,6 +30,7 @@ import { Subject } from 'rxjs';
 import { Task } from '../../mol-task';
 import { SizeValues } from '../../mol-gl/renderable/schema';
 import { Clipping } from '../../mol-theme/clipping';
+import { WebGLContext } from '../../mol-gl/webgl/context';
 
 export interface VolumeVisual<P extends VolumeParams> extends Visual<Volume, P> { }
 
@@ -48,6 +49,8 @@ interface VolumeVisualBuilder<P extends VolumeParams, G extends Geometry> {
     getLoci(pickingId: PickingId, volume: Volume, props: PD.Values<P>, id: number): Loci
     eachLocation(loci: Loci, volume: Volume, props: PD.Values<P>, apply: (interval: Interval) => boolean): boolean
     setUpdateState(state: VisualUpdateState, volume: Volume, newProps: PD.Values<P>, currentProps: PD.Values<P>, newTheme: Theme, currentTheme: Theme): void
+    mustRecreate?: (props: PD.Values<P>) => boolean
+    dispose?: (geometry: G) => void
 }
 
 interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry> extends VolumeVisualBuilder<P, G> {
@@ -55,7 +58,7 @@ interface VolumeVisualGeometryBuilder<P extends VolumeParams, G extends Geometry
 }
 
 export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geometry.Params<G>>(builder: VolumeVisualGeometryBuilder<P, G>, materialId: number): VolumeVisual<P> {
-    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState } = builder;
+    const { defaultProps, createGeometry, createLocationIterator, getLoci, eachLocation, setUpdateState, mustRecreate, dispose } = builder;
     const { updateValues, updateBoundingSphere, updateRenderableState, createPositionIterator } = builder.geometryUtils;
     const updateState = VisualUpdateState.create();
 
@@ -206,9 +209,10 @@ export function VolumeVisual<G extends Geometry, P extends VolumeParams & Geomet
             return Visual.setClipping(renderObject, clipping, lociApply, true);
         },
         destroy() {
-            // TODO
+            dispose?.(geometry);
             renderObject = undefined;
-        }
+        },
+        mustRecreate
     };
 }
 
@@ -224,8 +228,9 @@ export const VolumeParams = {
 };
 export type VolumeParams = typeof VolumeParams
 
-export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, P>, visualCtor: (materialId: number) => VolumeVisual<P>, getLoci: (volume: Volume, props: PD.Values<P>) => Loci): VolumeRepresentation<P> {
+export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx: RepresentationContext, getParams: RepresentationParamsGetter<Volume, P>, visualCtor: (materialId: number, props?: PD.Values<P>, webgl?: WebGLContext) => VolumeVisual<P>, getLoci: (volume: Volume, props: PD.Values<P>) => Loci): VolumeRepresentation<P> {
     let version = 0;
+    const { webgl } = ctx;
     const updated = new Subject<number>();
     const materialId = getNextMaterialId();
     const renderObjects: GraphicsRenderObject[] = [];
@@ -247,8 +252,13 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         Object.assign(_props, props, qualityProps);
 
         return Task.create('Creating or updating VolumeRepresentation', async runtime => {
-            if (!visual) visual = visualCtor(materialId);
-            const promise = visual.createOrUpdate({ webgl: ctx.webgl, runtime }, _theme, _props, volume);
+            if (!visual) {
+                visual = visualCtor(materialId, _props, webgl);
+            } else if (visual.mustRecreate?.(_props, webgl)) {
+                visual.destroy();
+                visual = visualCtor(materialId, _props, webgl);
+            }
+            const promise = visual.createOrUpdate({ webgl, runtime }, _theme, _props, volume);
             if (promise) await promise;
             // update list of renderObjects
             renderObjects.length = 0;

+ 2 - 0
src/mol-state/object.ts

@@ -80,6 +80,8 @@ interface StateObjectCell<T extends StateObject = StateObject, F extends StateTr
         values: any
     } | undefined,
 
+    paramsNormalizedVersion: string,
+
     dependencies: {
         dependentBy: StateObjectCell[],
         dependsOn: StateObjectCell[]

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio