Explorar o código

Merge branch 'master' of https://github.com/molstar/molstar into fix-qa-assignment

dsehnal %!s(int64=2) %!d(string=hai) anos
pai
achega
9cd4aeabaa

+ 2 - 0
CHANGELOG.md

@@ -6,12 +6,14 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Add an `includeTransparent` parameter to hide/show outlines of components that are transparent
 - Fix 'once' for animations of systems with many frames
 - Better guard against issue (black fringes) with bumpiness in impostors
 - Improve impostor shaders
     - Fix sphere near-clipping with orthographic projection
     - Fix cylinder near-clipping
     - Add interior cylinder caps
+    - Add per-pixel object clipping
 - Fix `QualityAssessment` assignment bug for structures with different auth vs label sequence numbering
 
 ## [v3.26.0] - 2022-12-04

+ 1 - 0
src/apps/docking-viewer/viewport.tsx

@@ -55,6 +55,7 @@ function occlusionStyle(plugin: PluginContext) {
                 scale: 1.0,
                 threshold: 0.33,
                 color: Color(0x0000),
+                includeTransparent: true,
             } },
             shadow: { name: 'off', params: {} },
         }

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

@@ -25,7 +25,7 @@ const Canvas3DPresets = {
         canvas3d: <Preset>{
             postprocessing: {
                 occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
-                outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000) } },
+                outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000), includeTransparent: true, } },
                 shadow: { name: 'off', params: {} },
             },
             renderer: {

+ 1 - 0
src/extensions/cellpack/model.ts

@@ -621,6 +621,7 @@ export const LoadCellPackModel = StateAction.build({
                         scale: 1,
                         threshold: 0.33,
                         color: ColorNames.black,
+                        includeTransparent: true,
                     }
                 }
             }

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

@@ -142,7 +142,7 @@ export class DrawPass {
         }
 
         if (PostprocessingPass.isEnabled(postprocessingProps)) {
-            if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
+            if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
                 this.depthTargetTransparent.bind();
                 renderer.clearDepth(true);
                 if (scene.opacityAverage < 1) {
@@ -196,7 +196,7 @@ export class DrawPass {
         }
 
         if (PostprocessingPass.isEnabled(postprocessingProps)) {
-            if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
+            if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
                 this.depthTargetTransparent.bind();
                 renderer.clearDepth(true);
                 if (scene.opacityAverage < 1) {
@@ -260,7 +260,7 @@ export class DrawPass {
                     this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
                 }
 
-                if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
+                if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
                     this.depthTargetTransparent.bind();
                     renderer.clearDepth(true);
                     if (scene.opacityAverage < 1) {

+ 27 - 7
src/mol-canvas3d/passes/postprocessing.ts

@@ -45,10 +45,12 @@ const OutlinesSchema = {
     uFar: UniformSpec('f'),
 
     uMaxPossibleViewZDiff: UniformSpec('f'),
+
+    dTransparentOutline: DefineSpec('boolean'),
 };
 type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
 
-function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture): OutlinesRenderable {
+function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
     const width = depthTextureOpaque.getWidth();
     const height = depthTextureOpaque.getHeight();
 
@@ -63,6 +65,8 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
         uFar: ValueCell.create(10000),
 
         uMaxPossibleViewZDiff: ValueCell.create(0.5),
+
+        dTransparentOutline: ValueCell.create(transparentOutline),
     };
 
     const schema = { ...OutlinesSchema };
@@ -288,10 +292,13 @@ const PostprocessingSchema = {
     dOutlineEnable: DefineSpec('boolean'),
     dOutlineScale: DefineSpec('number'),
     uOutlineThreshold: UniformSpec('f'),
+
+    dTransparentOutline: DefineSpec('boolean'),
 };
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
 
-function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture): PostprocessingRenderable {
+
+function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
     const values: Values<typeof PostprocessingSchema> = {
         ...QuadValues,
         tSsaoDepth: ValueCell.create(ssaoDepthTexture),
@@ -321,6 +328,8 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         dOutlineEnable: ValueCell.create(false),
         dOutlineScale: ValueCell.create(1),
         uOutlineThreshold: ValueCell.create(0.33),
+
+        dTransparentOutline: ValueCell.create(transparentOutline),
     };
 
     const schema = { ...PostprocessingSchema };
@@ -355,6 +364,7 @@ export const PostprocessingParams = {
             scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
             threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
             color: PD.Color(Color(0x000000)),
+            includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Draw outline around 3D objects' }),
@@ -373,8 +383,8 @@ export class PostprocessingPass {
         return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
     }
 
-    static isOutlineEnabled(props: PostprocessingProps) {
-        return props.outline.name === 'on';
+    static isTransparentOutlineEnabled(props: PostprocessingProps) {
+        return props.outline.name === 'on' && props.outline.params.includeTransparent;
     }
 
     readonly target: RenderTarget;
@@ -428,7 +438,7 @@ export class PostprocessingPass {
         this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
 
         this.outlinesTarget = webgl.createRenderTarget(width, height, false);
-        this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent);
+        this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
 
         this.shadowsTarget = webgl.createRenderTarget(width, height, false);
         this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
@@ -456,7 +466,7 @@ export class PostprocessingPass {
         this.ssaoRenderable = getSsaoRenderable(webgl, this.downsampleFactor === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
         this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
         this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
-        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture);
+        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true);
 
         this.background = new BackgroundPass(webgl, assetManager, width, height);
     }
@@ -494,6 +504,7 @@ export class PostprocessingPass {
         let needsUpdateMain = false;
         let needsUpdateSsao = false;
         let needsUpdateSsaoBlur = false;
+        let needsUpdateOutlines = false;
 
         const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
         const outlinesEnabled = props.outline.name === 'on';
@@ -615,7 +626,8 @@ export class PostprocessingPass {
         }
 
         if (props.outline.name === 'on') {
-            let { threshold } = props.outline.params;
+            let { threshold, includeTransparent } = props.outline.params;
+            const transparentOutline = includeTransparent ?? true;
             // orthographic needs lower threshold
             if (camera.state.mode === 'orthographic') threshold /= 5;
             const factor = Math.pow(1000, threshold) / 1000;
@@ -626,12 +638,16 @@ export class PostprocessingPass {
             ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
             ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
             ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
+            if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateOutlines = true; }
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
 
             ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
 
             ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
             if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
             ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
+            if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) { needsUpdateMain = true; }
+            ValueCell.updateIfChanged(this.renderable.values.dTransparentOutline, transparentOutline);
         }
 
         ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
@@ -650,6 +666,10 @@ export class PostprocessingPass {
         if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
         ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
 
+        if (needsUpdateOutlines) {
+            this.outlinesRenderable.update();
+        }
+
         if (needsUpdateShadows) {
             this.shadowsRenderable.update();
         }

+ 1 - 6
src/mol-gl/shader/chunks/clip-instance.glsl.ts

@@ -1,12 +1,7 @@
 export const clip_instance = `
 #if defined(dClipVariant_instance) && dClipObjectCount != 0
-    int flag = 0;
-    #if defined(dClipping)
-        flag = int(floor(vClipping * 255.0 + 0.5));
-    #endif
-
     vec4 mCenter = uModel * aTransform * vec4(uInvariantBoundingSphere.xyz, 1.0);
-    if (clipTest(vec4(mCenter.xyz, uInvariantBoundingSphere.w), flag))
+    if (clipTest(vec4(mCenter.xyz, uInvariantBoundingSphere.w)))
         // move out of [ -w, +w ] to 'discard' in vert shader
         gl_Position.z = 2.0 * gl_Position.w;
 #endif

+ 1 - 7
src/mol-gl/shader/chunks/clip-pixel.glsl.ts

@@ -1,12 +1,6 @@
 export const clip_pixel = `
 #if defined(dClipVariant_pixel) && dClipObjectCount != 0
-    #if defined(dClipping)
-        int clippingFlag = int(floor(vClipping * 255.0 + 0.5));
-    #else
-        int clippingFlag = 0;
-    #endif
-
-    if (clipTest(vec4(vModelPosition, 0.0), clippingFlag))
+    if (clipTest(vec4(vModelPosition, 0.0)))
         discard;
 #endif
 `;

+ 20 - 14
src/mol-gl/shader/chunks/common-clip.glsl.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Ludovic Autin <autin@scripps.edu>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -7,45 +7,45 @@
 
 export const common_clip = `
 #if dClipObjectCount != 0
-    vec3 quaternionTransform(vec4 q, vec3 v) {
+    vec3 quaternionTransform(const in vec4 q, const in vec3 v) {
         vec3 t = 2.0 * cross(q.xyz, v);
         return v + q.w * t + cross(q.xyz, t);
     }
 
-    vec4 computePlane(vec3 normal, vec3 inPoint) {
+    vec4 computePlane(const in vec3 normal, const in vec3 inPoint) {
         return vec4(normalize(normal), -dot(normal, inPoint));
     }
 
-    float planeSD(vec4 plane, vec3 center) {
+    float planeSD(const in vec4 plane, const in vec3 center) {
         return -dot(plane.xyz, center - plane.xyz * -plane.w);
     }
 
-    float sphereSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+    float sphereSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
         return (
             length(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position) / size) - 1.0
         ) * min(min(size.x, size.y), size.z);
     }
 
-    float cubeSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+    float cubeSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
         vec3 d = abs(quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position)) - size;
         return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
     }
 
-    float cylinderSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+    float cylinderSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
         vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
 
         vec2 d = abs(vec2(length(t.xz), t.y)) - size.xy;
         return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
     }
 
-    float infiniteConeSD(vec3 position, vec4 rotation, vec3 size, vec3 center) {
+    float infiniteConeSD(const in vec3 position, const in vec4 rotation, const in vec3 size, const in vec3 center) {
         vec3 t = quaternionTransform(vec4(-rotation.x, -rotation.y, -rotation.z, rotation.w), center - position);
 
         float q = length(t.xy);
         return dot(size.xy, vec2(q, t.z));
     }
 
-    float getSignedDistance(vec3 center, int type, vec3 position, vec4 rotation, vec3 scale) {
+    float getSignedDistance(const in vec3 center, const in int type, const in vec3 position, const in vec4 rotation, const in vec3 scale) {
         if (type == 1) {
             vec3 normal = quaternionTransform(rotation, vec3(0.0, 1.0, 0.0));
             vec4 plane = computePlane(normal, position);
@@ -65,7 +65,7 @@ export const common_clip = `
 
     #if __VERSION__ == 100
         // 8-bit
-        int bitwiseAnd(int a, int b) {
+        int bitwiseAnd(const in int a, const in int b) {
             int d = 128;
             int result = 0;
             for (int i = 0; i < 8; ++i) {
@@ -78,17 +78,23 @@ export const common_clip = `
             return result;
         }
 
-        bool hasBit(int mask, int bit) {
+        bool hasBit(const in int mask, const in int bit) {
             return bitwiseAnd(mask, bit) == 0;
         }
     #else
-        bool hasBit(int mask, int bit) {
+        bool hasBit(const in int mask, const in int bit) {
             return (mask & bit) == 0;
         }
     #endif
 
-    // flag is a bit-flag for clip-objects to ignore (note, object ids start at 1 not 0)
-    bool clipTest(vec4 sphere, int flag) {
+    bool clipTest(const in vec4 sphere) {
+        // flag is a bit-flag for clip-objects to ignore (note, object ids start at 1 not 0)
+        #if defined(dClipping)
+            int flag = int(floor(vClipping * 255.0 + 0.5));
+        #else
+            int flag = 0;
+        #endif
+
         #pragma unroll_loop_start
         for (int i = 0; i < dClipObjectCount; ++i) {
             if (flag == 0 || hasBit(flag, UNROLLED_LOOP_INDEX + 1)) {

+ 22 - 11
src/mol-gl/shader/cylinders.frag.ts

@@ -34,7 +34,7 @@ bool CylinderImpostor(
     in vec3 rayOrigin, in vec3 rayDir,
     in vec3 start, in vec3 end, in float radius,
     out vec3 cameraNormal, out bool interior,
-    out vec3 viewPosition, out float fragmentDepth
+    out vec3 modelPosition, out vec3 viewPosition, out float fragmentDepth
 ){
     vec3 ba = end - start;
     vec3 oc = rayOrigin - start;
@@ -60,8 +60,12 @@ bool CylinderImpostor(
     if (y > 0.0 && y < baba) {
         interior = false;
         cameraNormal = (oc + t * rayDir - ba * y / baba) / radius;
-        viewPosition = (uView * vec4(rayOrigin + t * rayDir, 1.0)).xyz;
+        modelPosition = rayOrigin + t * rayDir;
+        viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
         fragmentDepth = calcDepth(viewPosition);
+        #if defined(dClipVariant_pixel) && dClipObjectCount != 0
+            if (clipTest(vec4(modelPosition, 0.0))) fragmentDepth = -1.0;
+        #endif
         if (fragmentDepth > 0.0) return true;
     }
 
@@ -71,7 +75,8 @@ bool CylinderImpostor(
         if (abs(k1 + k2 * t) < h) {
             interior = false;
             cameraNormal = -ba / baba;
-            viewPosition = (uView * vec4(rayOrigin + t * rayDir, 1.0)).xyz;
+            modelPosition = rayOrigin + t * rayDir;
+            viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
             fragmentDepth = calcDepth(viewPosition);
             if (fragmentDepth > 0.0) return true;
         }
@@ -81,7 +86,8 @@ bool CylinderImpostor(
         if (abs(k1 + k2 * t) < h) {
             interior = false;
             cameraNormal = ba / baba;
-            viewPosition = (uView * vec4(rayOrigin + t * rayDir, 1.0)).xyz;
+            modelPosition = rayOrigin + t * rayDir;
+            viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
             fragmentDepth = calcDepth(viewPosition);
             if (fragmentDepth > 0.0) return true;
         }
@@ -95,7 +101,8 @@ bool CylinderImpostor(
         if (y > 0.0 && y < baba) {
             interior = true;
             cameraNormal = -(oc + t * rayDir - ba * y / baba) / radius;
-            viewPosition = (uView * vec4(rayOrigin + t * rayDir, 1.0)).xyz;
+            modelPosition = rayOrigin + t * rayDir;
+            viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
             fragmentDepth = calcDepth(viewPosition);
             return true;
         }
@@ -106,7 +113,8 @@ bool CylinderImpostor(
             if (abs(k1 + k2 * t) < -h) {
                 interior = true;
                 cameraNormal = ba / baba;
-                viewPosition = (uView * vec4(rayOrigin + t * rayDir, 1.0)).xyz;
+                modelPosition = rayOrigin + t * rayDir;
+                viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
                 fragmentDepth = calcDepth(viewPosition);
                 if (fragmentDepth > 0.0) return true;
             }
@@ -116,7 +124,8 @@ bool CylinderImpostor(
             if (abs(k1 + k2 * t) < -h) {
                 interior = true;
                 cameraNormal = -ba / baba;
-                viewPosition = (uView * vec4(rayOrigin + t * rayDir, 1.0)).xyz;
+                modelPosition = rayOrigin + t * rayDir;
+                viewPosition = (uView * vec4(modelPosition, 1.0)).xyz;
                 fragmentDepth = calcDepth(viewPosition);
                 if (fragmentDepth > 0.0) return true;
             }
@@ -127,15 +136,14 @@ bool CylinderImpostor(
 }
 
 void main() {
-    #include clip_pixel
-
     vec3 rayOrigin = vModelPosition;
     vec3 rayDir = mix(normalize(vModelPosition - uCameraPosition), uCameraDir, uIsOrtho);
 
     vec3 cameraNormal;
+    vec3 modelPosition;
     vec3 viewPosition;
     float fragmentDepth;
-    bool hit = CylinderImpostor(rayOrigin, rayDir, vStart, vEnd, vSize, cameraNormal, interior, viewPosition, fragmentDepth);
+    bool hit = CylinderImpostor(rayOrigin, rayDir, vStart, vEnd, vSize, cameraNormal, interior, modelPosition, viewPosition, fragmentDepth);
     if (!hit) discard;
 
     if (fragmentDepth < 0.0) discard;
@@ -143,7 +151,10 @@ void main() {
 
     gl_FragDepthEXT = fragmentDepth;
 
-    vec3 vModelPosition = (uInvView * vec4(viewPosition, 1.0)).xyz;
+    vec3 vViewPosition = viewPosition;
+    vec3 vModelPosition = modelPosition;
+
+    #include clip_pixel
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)

+ 6 - 1
src/mol-gl/shader/cylinders.vert.ts

@@ -55,7 +55,12 @@ void main() {
     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;
+    if(dot(camDir, dir) < 0.0) {
+        dir = -dir;
+        vec3 tmp = vStart;
+        vStart = vEnd;
+        vEnd = tmp;
+    }
 
     vec3 left = cross(camDir, dir);
     vec3 up = cross(left, dir);

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

@@ -229,7 +229,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
 
         #if defined(dClipVariant_pixel) && dClipObjectCount != 0
             vec3 vModelPosition = v3m4(unitPos * uGridDim, modelTransform);
-            if (clipTest(vec4(vModelPosition, 0.0), 0)) {
+            if (clipTest(vec4(vModelPosition, 0.0))) {
                 prevValue = value;
                 pos += step;
                 continue;

+ 5 - 1
src/mol-gl/shader/outlines.frag.ts

@@ -38,7 +38,11 @@ float getDepthOpaque(const in vec2 coords) {
 }
 
 float getDepthTransparent(const in vec2 coords) {
-    return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
+    #ifdef dTransparentOutline
+        return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
+    #else
+        return 1.0;
+    #endif
 }
 
 bool isBackground(const in float depth) {

+ 5 - 1
src/mol-gl/shader/postprocessing.frag.ts

@@ -51,7 +51,11 @@ float getDepthOpaque(const in vec2 coords) {
 }
 
 float getDepthTransparent(const in vec2 coords) {
-    return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
+    #ifdef dTransparentOutline
+        return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
+    #else
+        return 1.0;
+    #endif
 }
 
 bool isBackground(const in float depth) {

+ 18 - 7
src/mol-gl/shader/spheres.frag.ts

@@ -23,7 +23,7 @@ varying float vRadiusSq;
 varying vec3 vPoint;
 varying vec3 vPointViewPosition;
 
-bool SphereImpostor(out vec3 cameraPos, out vec3 cameraNormal, out bool interior, out float fragmentDepth){
+bool SphereImpostor(out vec3 modelPos, out vec3 cameraPos, out vec3 cameraNormal, out bool interior, out float fragmentDepth, out bool clipped){
     vec3 cameraSpherePos = -vPointViewPosition;
 
     vec3 rayOrigin = mix(vec3(0.0, 0.0, 0.0), vPoint, uIsOrtho);
@@ -40,14 +40,23 @@ bool SphereImpostor(out vec3 cameraPos, out vec3 cameraNormal, out bool interior
     float negT = mix(B - sqrtDet, B + sqrtDet, uIsOrtho);
 
     cameraPos = rayDirection * negT + rayOrigin;
+    modelPos = (uInvView * vec4(cameraPos, 1.0)).xyz;
     fragmentDepth = calcDepth(cameraPos);
 
+    #if defined(dClipVariant_pixel) && dClipObjectCount != 0
+        if (clipTest(vec4(modelPos, 0.0))) {
+            clipped = true;
+            fragmentDepth = -1.0;
+        }
+    #endif
+
     if (fragmentDepth > 0.0) {
         cameraNormal = normalize(cameraPos - cameraSpherePos);
         interior = false;
         return true;
     } else if (uDoubleSided) {
         cameraPos = rayDirection * posT + rayOrigin;
+        modelPos = (uInvView * vec4(cameraPos, 1.0)).xyz;
         fragmentDepth = calcDepth(cameraPos);
         cameraNormal = -normalize(cameraPos - cameraSpherePos);
         interior = true;
@@ -58,25 +67,27 @@ bool SphereImpostor(out vec3 cameraPos, out vec3 cameraNormal, out bool interior
 }
 
 void main(void){
-    #include clip_pixel
-
+    vec3 modelPos;
     vec3 cameraPos;
     vec3 cameraNormal;
     float fragmentDepth;
-    bool hit = SphereImpostor(cameraPos, cameraNormal, interior, fragmentDepth);
+    bool clipped = false;
+    bool hit = SphereImpostor(modelPos, cameraPos, cameraNormal, interior, fragmentDepth, clipped);
     if (!hit) discard;
 
     if (fragmentDepth < 0.0) discard;
     if (fragmentDepth > 1.0) discard;
 
-    if (interior) {
+    vec3 vViewPosition = cameraPos;
+    vec3 vModelPosition = modelPos;
+
+    if (interior && !clipped) {
         fragmentDepth = 0.0 + (0.0000001 / vRadius);
     }
 
     gl_FragDepthEXT = fragmentDepth;
 
-    vec3 vViewPosition = cameraPos;
-    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
+    #include clip_pixel
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)

+ 2 - 2
src/mol-plugin-ui/structure/quick-styles.tsx

@@ -56,7 +56,7 @@ export class QuickStyles extends PurePluginUIComponent {
                 postprocessing: {
                     outline: {
                         name: 'on',
-                        params: { scale: 1, color: Color(0x000000), threshold: 0.25 }
+                        params: { scale: 1, color: Color(0x000000), threshold: 0.25, includeTransparent: true }
                     },
                     occlusion: {
                         name: 'on',
@@ -79,7 +79,7 @@ export class QuickStyles extends PurePluginUIComponent {
                         name: 'on',
                         params: pp.outline.name === 'on'
                             ? pp.outline.params
-                            : { scale: 1, color: Color(0x000000), threshold: 0.33 }
+                            : { scale: 1, color: Color(0x000000), threshold: 0.33, includeTransparent: true }
                     },
                     occlusion: {
                         name: 'on',