ソースを参照

Merge pull request #737 from molstar/pp-improvements

Post-processing improvements
Alexander Rose 2 年 前
コミット
f2399d3179

+ 4 - 0
CHANGELOG.md

@@ -6,6 +6,10 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Add occlusion color parameter
+- Fix issue with outlines and orthographic camera
+- Reduce over-blurring occlusion at larger view distances
+
 ## [v3.31.4] - 2023-02-24
 
 - Allow link cylinder/line `dashCount` set to '0'

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -50,6 +50,7 @@ function occlusionStyle(plugin: PluginContext) {
                 radius: 5,
                 samples: 32,
                 resolutionScale: 1,
+                color: Color(0x000000),
             } },
             outline: { name: 'on', params: {
                 scale: 1.0,

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -24,7 +24,7 @@ const Canvas3DPresets = {
     illustrative: {
         canvas3d: <Preset>{
             postprocessing: {
-                occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
+                occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1, color: Color(0x000000) } },
                 outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000), includeTransparent: true, } },
                 shadow: { name: 'off', params: {} },
             },

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Ludovic Autin <ludovic.autin@gmail.com>
@@ -604,6 +604,7 @@ export const LoadCellPackModel = StateAction.build({
                         bias: 1,
                         blurKernelSize: 15,
                         resolutionScale: 1,
+                        color: Color(0x000000),
                     }
                 },
                 shadow: {

+ 59 - 49
src/mol-canvas3d/passes/postprocessing.ts

@@ -43,9 +43,9 @@ const OutlinesSchema = {
     dOrthographic: DefineSpec('number'),
     uNear: UniformSpec('f'),
     uFar: UniformSpec('f'),
+    uInvProjection: UniformSpec('m4'),
 
-    uMaxPossibleViewZDiff: UniformSpec('f'),
-
+    uOutlineThreshold: UniformSpec('f'),
     dTransparentOutline: DefineSpec('boolean'),
 };
 type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
@@ -63,9 +63,9 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
         dOrthographic: ValueCell.create(0),
         uNear: ValueCell.create(1),
         uFar: ValueCell.create(10000),
+        uInvProjection: ValueCell.create(Mat4.identity()),
 
-        uMaxPossibleViewZDiff: ValueCell.create(0.5),
-
+        uOutlineThreshold: ValueCell.create(0.33),
         dTransparentOutline: ValueCell.create(transparentOutline),
     };
 
@@ -189,8 +189,7 @@ const SsaoBlurSchema = {
     uBlurDirectionX: UniformSpec('f'),
     uBlurDirectionY: UniformSpec('f'),
 
-    uMaxPossibleViewZDiff: UniformSpec('f'),
-
+    uInvProjection: UniformSpec('m4'),
     uNear: UniformSpec('f'),
     uFar: UniformSpec('f'),
     uBounds: UniformSpec('v4'),
@@ -211,8 +210,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
         uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
         uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
 
-        uMaxPossibleViewZDiff: ValueCell.create(0.5),
-
+        uInvProjection: ValueCell.create(Mat4.identity()),
         uNear: ValueCell.create(0.0),
         uFar: ValueCell.create(10000.0),
         uBounds: ValueCell.create(Vec4()),
@@ -280,11 +278,9 @@ const PostprocessingSchema = {
     uFogFar: UniformSpec('f'),
     uFogColor: UniformSpec('v3'),
     uOutlineColor: UniformSpec('v3'),
+    uOcclusionColor: UniformSpec('v3'),
     uTransparentBackground: UniformSpec('b'),
 
-    uMaxPossibleViewZDiff: UniformSpec('f'),
-    uInvProjection: UniformSpec('m4'),
-
     dOcclusionEnable: DefineSpec('boolean'),
     uOcclusionOffset: UniformSpec('v2'),
 
@@ -292,8 +288,6 @@ const PostprocessingSchema = {
 
     dOutlineEnable: DefineSpec('boolean'),
     dOutlineScale: DefineSpec('number'),
-    uOutlineThreshold: UniformSpec('f'),
-
     dTransparentOutline: DefineSpec('boolean'),
 };
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
@@ -317,11 +311,9 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         uFogFar: ValueCell.create(10000),
         uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
         uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
+        uOcclusionColor: ValueCell.create(Vec3.create(0, 0, 0)),
         uTransparentBackground: ValueCell.create(false),
 
-        uMaxPossibleViewZDiff: ValueCell.create(0.5),
-        uInvProjection: ValueCell.create(Mat4.identity()),
-
         dOcclusionEnable: ValueCell.create(true),
         uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
 
@@ -329,8 +321,6 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
 
         dOutlineEnable: ValueCell.create(false),
         dOutlineScale: ValueCell.create(1),
-        uOutlineThreshold: ValueCell.create(0.33),
-
         dTransparentOutline: ValueCell.create(transparentOutline),
     };
 
@@ -349,6 +339,7 @@ export const PostprocessingParams = {
             bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
             blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
             resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
+            color: PD.Color(Color(0x000000)),
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
@@ -543,11 +534,14 @@ export class PostprocessingPass {
             ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
             ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
 
+            ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection);
+            ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection);
+
             if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
                 needsUpdateSsaoBlur = true;
+                ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
+                ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
             }
-            ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
-            ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
 
             if (this.nSamples !== props.occlusion.params.samples) {
                 needsUpdateSsao = true;
@@ -567,8 +561,8 @@ export class PostprocessingPass {
 
                 ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
                 ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
-                ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
-                ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
+                ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
+                ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
             }
 
             if (this.downsampleFactor !== props.occlusion.params.resolutionScale) {
@@ -595,6 +589,8 @@ export class PostprocessingPass {
                 ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
                 ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
             }
+
+            ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color));
         }
 
         if (props.shadow.name === 'on') {
@@ -611,7 +607,10 @@ export class PostprocessingPass {
 
             ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
             ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
-            ValueCell.updateIfChanged(this.shadowsRenderable.values.dOrthographic, orthographic);
+            if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) {
+                ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic);
+                needsUpdateShadows = true;
+            }
 
             ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
             ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
@@ -630,30 +629,33 @@ export class PostprocessingPass {
         }
 
         if (props.outline.name === 'on') {
-            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 / 10) / 1000;
-            // use radiusMax for stable outlines when zooming
-            const maxPossibleViewZDiff = factor * camera.state.radiusMax;
+            const transparentOutline = props.outline.params.includeTransparent ?? true;
             const outlineScale = props.outline.params.scale - 1;
+            const outlineThreshold = 50 * props.outline.params.threshold;
 
             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.outlinesRenderable.values.uInvProjection, invProjection);
+            if (this.outlinesRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
+                needsUpdateOutlines = true;
+                ValueCell.update(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
+            }
+            if (this.outlinesRenderable.values.dOrthographic.ref.value !== orthographic) {
+                needsUpdateOutlines = true;
+                ValueCell.update(this.outlinesRenderable.values.dOrthographic, orthographic);
+            }
+            ValueCell.updateIfChanged(this.outlinesRenderable.values.uOutlineThreshold, outlineThreshold);
 
             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);
-            ValueCell.update(this.renderable.values.uInvProjection, invProjection);
-
-            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);
+            if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
+                needsUpdateMain = true;
+                ValueCell.update(this.renderable.values.dOutlineScale, outlineScale);
+            }
+            if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) {
+                needsUpdateMain = true;
+                ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline);
+            }
         }
 
         ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
@@ -662,15 +664,23 @@ export class PostprocessingPass {
         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.dShadowEnable.ref.value !== shadowsEnabled) { needsUpdateMain = true; }
-        ValueCell.updateIfChanged(this.renderable.values.dShadowEnable, shadowsEnabled);
-        if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
-        ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
+        if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
+            needsUpdateMain = true;
+            ValueCell.update(this.renderable.values.dOrthographic, orthographic);
+        }
+
+        if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) {
+            needsUpdateMain = true;
+            ValueCell.update(this.renderable.values.dOutlineEnable, outlinesEnabled);
+        }
+        if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) {
+            needsUpdateMain = true;
+            ValueCell.update(this.renderable.values.dShadowEnable, shadowsEnabled);
+        }
+        if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) {
+            needsUpdateMain = true;
+            ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
+        }
 
         if (needsUpdateOutlines) {
             this.outlinesRenderable.update();

+ 13 - 4
src/mol-gl/shader/outlines.frag.ts

@@ -16,8 +16,9 @@ uniform vec2 uTexSize;
 
 uniform float uNear;
 uniform float uFar;
+uniform mat4 uInvProjection;
 
-uniform float uMaxPossibleViewZDiff;
+uniform float uOutlineThreshold;
 
 #include common
 
@@ -49,17 +50,25 @@ bool isBackground(const in float depth) {
     return depth == 1.0;
 }
 
+float getPixelSize(const in vec2 coords, const in float depth) {
+    vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
+    vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
+    return distance(viewPos0, viewPos1);
+}
+
 void main(void) {
-    float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
+    float backgroundViewZ = 2.0 * uFar;
 
     vec2 coords = gl_FragCoord.xy / uTexSize;
     vec2 invTexSize = 1.0 / uTexSize;
 
     float selfDepthOpaque = getDepthOpaque(coords);
     float selfViewZOpaque = isBackground(selfDepthOpaque) ? backgroundViewZ : getViewZ(selfDepthOpaque);
+    float pixelSizeOpaque = getPixelSize(coords, selfDepthOpaque) * uOutlineThreshold;
 
     float selfDepthTransparent = getDepthTransparent(coords);
     float selfViewZTransparent = isBackground(selfDepthTransparent) ? backgroundViewZ : getViewZ(selfDepthTransparent);
+    float pixelSizeTransparent = getPixelSize(coords, selfDepthTransparent) * uOutlineThreshold;
 
     float outline = 1.0;
     float bestDepth = 1.0;
@@ -73,14 +82,14 @@ void main(void) {
             float sampleDepthTransparent = getDepthTransparent(sampleCoords);
 
             float sampleViewZOpaque = isBackground(sampleDepthOpaque) ? backgroundViewZ : getViewZ(sampleDepthOpaque);
-            if (abs(selfViewZOpaque - sampleViewZOpaque) > uMaxPossibleViewZDiff && selfDepthOpaque > sampleDepthOpaque && sampleDepthOpaque <= bestDepth) {
+            if (abs(selfViewZOpaque - sampleViewZOpaque) > pixelSizeOpaque && selfDepthOpaque > sampleDepthOpaque && sampleDepthOpaque <= bestDepth) {
                 outline = 0.0;
                 bestDepth = sampleDepthOpaque;
             }
 
             if (sampleDepthTransparent < sampleDepthOpaque) {
                 float sampleViewZTransparent = isBackground(sampleDepthTransparent) ? backgroundViewZ : getViewZ(sampleDepthTransparent);
-                if (abs(selfViewZTransparent - sampleViewZTransparent) > uMaxPossibleViewZDiff && selfDepthTransparent > sampleDepthTransparent && sampleDepthTransparent <= bestDepth) {
+                if (abs(selfViewZTransparent - sampleViewZTransparent) > pixelSizeTransparent && selfDepthTransparent > sampleDepthTransparent && sampleDepthTransparent <= bestDepth) {
                     outline = 0.0;
                     bestDepth = sampleDepthTransparent;
                     transparentFlag = 1.0;

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

@@ -24,16 +24,10 @@ uniform float uFogNear;
 uniform float uFogFar;
 uniform vec3 uFogColor;
 uniform vec3 uOutlineColor;
+uniform vec3 uOcclusionColor;
 uniform bool uTransparentBackground;
-
 uniform vec2 uOcclusionOffset;
 
-uniform float uMaxPossibleViewZDiff;
-uniform mat4 uInvProjection;
-
-const float outlineDistanceFactor = 5.0;
-const vec3 occlusionColor = vec3(0.0);
-
 #include common
 
 float getViewZ(const in float depth) {
@@ -64,21 +58,14 @@ bool isBackground(const in float depth) {
     return depth == 1.0;
 }
 
-float getPixelSize(const in vec2 coords, const in float depth) {
-    vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
-    vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
-    return distance(viewPos0, viewPos1);
-}
-
 float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
-    float backgroundViewZ = uFar + 3.0 * uMaxPossibleViewZDiff;
+    float backgroundViewZ = 2.0 * uFar;
     vec2 invTexSize = 1.0 / uTexSize;
 
     float transparentDepth = getDepthTransparent(coords);
     float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
     float transparentSelfViewZ = isBackground(transparentDepth) ? backgroundViewZ : getViewZ(transparentDepth);
     float selfDepth = min(opaqueDepth, transparentDepth);
-    float pixelSize = getPixelSize(coords, selfDepth);
 
     float outline = 1.0;
     closestTexel = 1.0;
@@ -96,7 +83,7 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
             float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
 
             float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
-            if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel && abs(selfViewZ - sampleOutlineViewZ) > uMaxPossibleViewZDiff + (pixelSize * outlineDistanceFactor)) {
+            if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
                 outline = 0.0;
                 closestTexel = sampleOutlineDepth;
             }
@@ -130,9 +117,9 @@ void main(void) {
             fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
             float occlusionFactor = getSsao(coords + uOcclusionOffset);
             if (!uTransparentBackground) {
-                color.rgb = mix(mix(occlusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
+                color.rgb = mix(mix(uOcclusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
             } else {
-                color.rgb = mix(occlusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
+                color.rgb = mix(uOcclusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
             }
         }
     #endif

+ 14 - 5
src/mol-gl/shader/ssao-blur.frag.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -19,8 +19,7 @@ uniform float uKernel[dOcclusionKernelSize];
 uniform float uBlurDirectionX;
 uniform float uBlurDirectionY;
 
-uniform float uMaxPossibleViewZDiff;
-
+uniform mat4 uInvProjection;
 uniform float uNear;
 uniform float uFar;
 
@@ -42,6 +41,12 @@ bool outsideBounds(const in vec2 p) {
     return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
 }
 
+float getPixelSize(const in vec2 coords, const in float depth) {
+    vec3 viewPos0 = screenSpaceToViewSpace(vec3(coords, depth), uInvProjection);
+    vec3 viewPos1 = screenSpaceToViewSpace(vec3(coords + vec2(1.0, 0.0) / uTexSize, depth), uInvProjection);
+    return distance(viewPos0, viewPos1);
+}
+
 void main(void) {
     vec2 coords = gl_FragCoord.xy / uTexSize;
 
@@ -60,6 +65,8 @@ void main(void) {
     }
 
     float selfViewZ = getViewZ(selfDepth);
+    float pixelSize = getPixelSize(coords, selfDepth);
+    float maxDiffViewZ = pixelSize * 5.0;
 
     vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
 
@@ -67,6 +74,8 @@ void main(void) {
     float kernelSum = 0.0;
     // only if kernelSize is odd
     for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) {
+        if (abs(float(i)) > 1.0 && abs(float(i)) * pixelSize > 0.5) continue;
+
         vec2 sampleCoords = coords + float(i) * offset;
         if (outsideBounds(sampleCoords)) {
             continue;
@@ -79,9 +88,9 @@ void main(void) {
             continue;
         }
 
-        if (abs(float(i)) > 1.0) { // abs is not defined for int in webgl1
+        if (abs(float(i)) > 1.0) {
             float sampleViewZ = getViewZ(sampleDepth);
-            if (abs(selfViewZ - sampleViewZ) > uMaxPossibleViewZDiff) {
+            if (abs(selfViewZ - sampleViewZ) > maxDiffViewZ) {
                 continue;
             }
         }

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

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -60,7 +60,7 @@ export class QuickStyles extends PurePluginUIComponent {
                     },
                     occlusion: {
                         name: 'on',
-                        params: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1 }
+                        params: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1, color: Color(0x000000) }
                     },
                     shadow: { name: 'off', params: {} },
                 }
@@ -85,7 +85,7 @@ export class QuickStyles extends PurePluginUIComponent {
                         name: 'on',
                         params: pp.occlusion.name === 'on'
                             ? pp.occlusion.params
-                            : { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1 }
+                            : { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1, color: Color(0x000000) }
                     },
                     shadow: { name: 'off', params: {} },
                 }

+ 1 - 0
src/mol-plugin/util/headless-screenshot.ts

@@ -210,6 +210,7 @@ export const STYLIZED_POSTPROCESSING: Partial<PostprocessingProps> = {
             bias: 0.8,
             blurKernelSize: 15,
             resolutionScale: 1,
+            color: ColorNames.black,
         }
     }, outline: {
         name: 'on' as const, params: {