Browse Source

Merge pull request #762 from molstar/multi-scale-ssao

add multi-scale ssao
Alexander Rose 2 years ago
parent
commit
a3267dafdb

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

@@ -45,9 +45,10 @@ function occlusionStyle(plugin: PluginContext) {
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
             occlusion: { name: 'on', params: {
-                bias: 0.8,
                 blurKernelSize: 15,
+                multiScale: { name: 'off', params: {} },
                 radius: 5,
+                bias: 0.8,
                 samples: 32,
                 resolutionScale: 1,
                 color: Color(0x000000),

+ 44 - 6
src/examples/lighting/index.ts

@@ -24,9 +24,31 @@ const Canvas3DPresets = {
     illustrative: {
         canvas3d: <Preset>{
             postprocessing: {
-                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: {} },
+                occlusion: {
+                    name: 'on',
+                    params: {
+                        samples: 32,
+                        multiScale: { name: 'off', params: {} },
+                        radius: 5,
+                        bias: 0.8,
+                        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: {}
+                },
             },
             renderer: {
                 ambientIntensity: 1.0,
@@ -37,9 +59,25 @@ const Canvas3DPresets = {
     occlusion: {
         canvas3d: <Preset>{
             postprocessing: {
-                occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
-                outline: { name: 'off', params: {} },
-                shadow: { name: 'off', params: {} },
+                occlusion: {
+                    name: 'on',
+                    params: {
+                        samples: 32,
+                        multiScale: { name: 'off', params: {} },
+                        radius: 5,
+                        bias: 0.8,
+                        blurKernelSize: 15,
+                        resolutionScale: 1,
+                    }
+                },
+                outline: {
+                    name: 'off',
+                    params: {}
+                },
+                shadow: {
+                    name: 'off',
+                    params: {}
+                },
             },
             renderer: {
                 ambientIntensity: 0.4,

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

@@ -600,6 +600,7 @@ export const LoadCellPackModel = StateAction.build({
                     name: 'on',
                     params: {
                         samples: 32,
+                        multiScale: { name: 'off', params: {} },
                         radius: 8,
                         bias: 1,
                         blurKernelSize: 15,

+ 148 - 12
src/mol-canvas3d/passes/postprocessing.ts

@@ -11,7 +11,7 @@ import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/rende
 import { ShaderCode } from '../../mol-gl/shader-code';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { Texture } from '../../mol-gl/webgl/texture';
-import { ValueCell } from '../../mol-util';
+import { deepEqual, ValueCell } from '../../mol-util';
 import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
 import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
@@ -137,6 +137,8 @@ function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): Shadows
 const SsaoSchema = {
     ...QuadSchema,
     tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
 
     uSamples: UniformSpec('v3[]'),
     dNSamples: DefineSpec('number'),
@@ -149,14 +151,23 @@ const SsaoSchema = {
 
     uRadius: UniformSpec('f'),
     uBias: UniformSpec('f'),
+
+    dMultiScale: DefineSpec('boolean'),
+    dLevels: DefineSpec('number'),
+    uLevelRadius: UniformSpec('f[]'),
+    uLevelBias: UniformSpec('f[]'),
+    uNearThreshold: UniformSpec('f'),
+    uFarThreshold: UniformSpec('f'),
 };
 
 type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
 
-function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRenderable {
+function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
     const values: Values<typeof SsaoSchema> = {
         ...QuadValues,
         tDepth: ValueCell.create(depthTexture),
+        tDepthHalf: ValueCell.create(depthHalfTexture),
+        tDepthQuarter: ValueCell.create(depthQuarterTexture),
 
         uSamples: ValueCell.create(getSamples(32)),
         dNSamples: ValueCell.create(32),
@@ -167,8 +178,15 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
 
         uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
 
-        uRadius: ValueCell.create(8.0),
-        uBias: ValueCell.create(0.025),
+        uRadius: ValueCell.create(Math.pow(2, 5)),
+        uBias: ValueCell.create(0.8),
+
+        dMultiScale: ValueCell.create(false),
+        dLevels: ValueCell.create(3),
+        uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
+        uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
+        uNearThreshold: ValueCell.create(10.0),
+        uFarThreshold: ValueCell.create(1500.0),
     };
 
     const schema = { ...SsaoSchema };
@@ -292,7 +310,6 @@ const PostprocessingSchema = {
 };
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
 
-
 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,
@@ -335,7 +352,23 @@ export const PostprocessingParams = {
     occlusion: PD.MappedStatic('on', {
         on: PD.Group({
             samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
-            radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
+            multiScale: PD.MappedStatic('off', {
+                on: PD.Group({
+                    levels: PD.ObjectList({
+                        radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
+                        bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
+                    }, o => `${o.radius}, ${o.bias}`, { defaultValue: [
+                        { radius: 2, bias: 1 },
+                        { radius: 5, bias: 1 },
+                        { radius: 8, bias: 1 },
+                        { radius: 11, bias: 1 },
+                    ] }),
+                    nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
+                    farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
+                }),
+                off: PD.Group({})
+            }, { cycle: true }),
+            radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
             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' }),
@@ -371,6 +404,27 @@ export const PostprocessingParams = {
 
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
+type Levels = {
+    count: number
+    radius: number[]
+    bias: number[]
+}
+
+function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
+    const count = props.length;
+    const { radius, bias } = levels || {
+        radius: (new Array(count * 3)).fill(0),
+        bias: (new Array(count * 3)).fill(0),
+    };
+    props = props.slice().sort((a, b) => a.radius - b.radius);
+    for (let i = 0; i < count; ++i) {
+        const p = props[i];
+        radius[i] = Math.pow(2, p.radius);
+        bias[i] = p.bias;
+    }
+    return { count, radius, bias };
+}
+
 export class PostprocessingPass {
     static isEnabled(props: PostprocessingProps) {
         return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
@@ -395,6 +449,12 @@ export class PostprocessingPass {
     private readonly downsampledDepthTarget: RenderTarget;
     private readonly downsampleDepthRenderable: CopyRenderable;
 
+    private readonly depthHalfTarget: RenderTarget;
+    private readonly depthHalfRenderable: CopyRenderable;
+
+    private readonly depthQuarterTarget: RenderTarget;
+    private readonly depthQuarterRenderable: CopyRenderable;
+
     private readonly ssaoDepthTexture: Texture;
     private readonly ssaoDepthBlurProxyTexture: Texture;
 
@@ -414,6 +474,8 @@ export class PostprocessingPass {
         return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
     }
 
+    private levels: { radius: number, bias: number }[];
+
     private readonly bgColor = Vec3();
     readonly background: BackgroundPass;
 
@@ -426,6 +488,7 @@ export class PostprocessingPass {
         this.blurKernelSize = 1;
         this.downsampleFactor = 1;
         this.ssaoScale = this.calcSsaoScale();
+        this.levels = [];
 
         // needs to be linear for anti-aliasing pass
         this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
@@ -443,11 +506,27 @@ export class PostprocessingPass {
         const sw = Math.floor(width * this.ssaoScale);
         const sh = Math.floor(height * this.ssaoScale);
 
+        const hw = Math.floor(sw * 0.5);
+        const hh = Math.floor(sh * 0.5);
+
+        const qw = Math.floor(sw * 0.25);
+        const qh = Math.floor(sh * 0.25);
+
         this.downsampledDepthTarget = drawPass.packedDepth
             ? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
             : webgl.createRenderTarget(sw, sh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
         this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
 
+        this.depthHalfTarget = drawPass.packedDepth
+            ? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba')
+            : webgl.createRenderTarget(hw, hh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
+        this.depthHalfRenderable = createCopyRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
+
+        this.depthQuarterTarget = drawPass.packedDepth
+            ? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
+            : webgl.createRenderTarget(qw, qh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
+        this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
+
         this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
         this.ssaoDepthTexture.define(sw, sh);
         this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
@@ -458,7 +537,7 @@ export class PostprocessingPass {
 
         this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
 
-        this.ssaoRenderable = getSsaoRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
+        this.ssaoRenderable = getSsaoRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture, this.depthHalfTarget.texture, this.depthQuarterTarget.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, true);
@@ -473,19 +552,30 @@ export class PostprocessingPass {
         if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
             this.ssaoScale = ssaoScale;
 
-            const sw = Math.floor(width * this.ssaoScale);
-            const sh = Math.floor(height * this.ssaoScale);
             this.target.setSize(width, height);
             this.outlinesTarget.setSize(width, height);
             this.shadowsTarget.setSize(width, height);
+
+            const sw = Math.floor(width * this.ssaoScale);
+            const sh = Math.floor(height * this.ssaoScale);
             this.downsampledDepthTarget.setSize(sw, sh);
             this.ssaoDepthTexture.define(sw, sh);
             this.ssaoDepthBlurProxyTexture.define(sw, sh);
 
+            const hw = Math.floor(sw * 0.5);
+            const hh = Math.floor(sh * 0.5);
+            this.depthHalfTarget.setSize(hw, hh);
+
+            const qw = Math.floor(sw * 0.25);
+            const qh = Math.floor(sh * 0.25);
+            this.depthQuarterTarget.setSize(qw, qh);
+
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
             ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
+            ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
+            ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
             ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
             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));
@@ -550,7 +640,30 @@ export class PostprocessingPass {
                 ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
                 ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
             }
-            ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
+
+            const multiScale = props.occlusion.params.multiScale.name === 'on';
+            if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
+                needsUpdateSsao = true;
+                ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
+            }
+
+            if (props.occlusion.params.multiScale.name === 'on') {
+                const mp = props.occlusion.params.multiScale.params;
+                if (!deepEqual(this.levels, mp.levels)) {
+                    needsUpdateSsao = true;
+
+                    this.levels = mp.levels;
+                    const levels = getLevels(mp.levels);
+                    ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count);
+
+                    ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius);
+                    ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias);
+                }
+                ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold);
+                ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold);
+            } else {
+                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) {
@@ -573,18 +686,30 @@ export class PostprocessingPass {
 
                 const sw = Math.floor(w * this.ssaoScale);
                 const sh = Math.floor(h * this.ssaoScale);
-
                 this.downsampledDepthTarget.setSize(sw, sh);
                 this.ssaoDepthTexture.define(sw, sh);
                 this.ssaoDepthBlurProxyTexture.define(sw, sh);
 
+                const hw = Math.floor(sw * 0.5);
+                const hh = Math.floor(sh * 0.5);
+                this.depthHalfTarget.setSize(hw, hh);
+
+                const qw = Math.floor(sw * 0.25);
+                const qh = Math.floor(sh * 0.25);
+                this.depthQuarterTarget.setSize(qw, qh);
+
                 if (this.ssaoScale === 1) {
                     ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTextureOpaque);
                 } else {
                     ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture);
                 }
 
+                ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture);
+                ValueCell.update(this.ssaoRenderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
+
                 ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
+                ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
+                ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
                 ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
                 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));
@@ -743,10 +868,22 @@ export class PostprocessingPass {
             state.scissor(sx, sy, sw, sh);
 
             if (this.ssaoScale < 1) {
+                if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
                 this.downsampledDepthTarget.bind();
                 this.downsampleDepthRenderable.render();
+                if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
             }
 
+            if (isTimingMode) this.webgl.timer.mark('SSAO.half');
+            this.depthHalfTarget.bind();
+            this.depthHalfRenderable.render();
+            if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
+
+            if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
+            this.depthQuarterTarget.bind();
+            this.depthQuarterRenderable.render();
+            if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
+
             this.ssaoFramebuffer.bind();
             this.ssaoRenderable.render();
 
@@ -862,4 +999,3 @@ export class AntialiasingPass {
         }
     }
 }
-

+ 5 - 4
src/mol-gl/renderer.ts

@@ -131,18 +131,19 @@ export type Light = {
 const tmpDir = Vec3();
 const tmpColor = Vec3();
 function getLight(props: RendererProps['light'], light?: Light): Light {
+    const count = props.length;
     const { direction, color } = light || {
-        direction: (new Array(5 * 3)).fill(0),
-        color: (new Array(5 * 3)).fill(0),
+        direction: (new Array(count * 3)).fill(0),
+        color: (new Array(count * 3)).fill(0),
     };
-    for (let i = 0, il = props.length; i < il; ++i) {
+    for (let i = 0; i < count; ++i) {
         const p = props[i];
         Vec3.directionFromSpherical(tmpDir, degToRad(p.inclination), degToRad(p.azimuth), 1);
         Vec3.toArray(tmpDir, direction, i * 3);
         Vec3.scale(tmpColor, Color.toVec3Normalized(tmpColor, p.color), p.intensity);
         Vec3.toArray(tmpColor, color, i * 3);
     }
-    return { count: props.length, direction, color };
+    return { count, direction, color };
 }
 
 namespace Renderer {

+ 85 - 26
src/mol-gl/shader/ssao.frag.ts

@@ -13,6 +13,8 @@ precision highp sampler2D;
 #include common
 
 uniform sampler2D tDepth;
+uniform sampler2D tDepthHalf;
+uniform sampler2D tDepthQuarter;
 uniform vec2 uTexSize;
 uniform vec4 uBounds;
 
@@ -21,7 +23,14 @@ uniform vec3 uSamples[dNSamples];
 uniform mat4 uProjection;
 uniform mat4 uInvProjection;
 
-uniform float uRadius;
+#ifdef dMultiScale
+    uniform float uLevelRadius[dLevels];
+    uniform float uLevelBias[dLevels];
+    uniform float uNearThreshold;
+    uniform float uFarThreshold;
+#else
+    uniform float uRadius;
+#endif
 uniform float uBias;
 
 float smootherstep(float edge0, float edge1, float x) {
@@ -46,20 +55,38 @@ bool isBackground(const in float depth) {
     return depth == 1.0;
 }
 
-bool outsideBounds(const in vec2 p) {
-    return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
+float getDepth(const in vec2 coords) {
+    vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
+    #ifdef depthTextureSupport
+        return texture2D(tDepth, c).r;
+    #else
+        return unpackRGBAToDepth(texture2D(tDepth, c));
+    #endif
 }
 
-float getDepth(const in vec2 coords) {
-    if (outsideBounds(coords)) {
-        return 1.0;
-    } else {
-        #ifdef depthTextureSupport
-            return texture2D(tDepth, coords).r;
-        #else
-            return unpackRGBAToDepth(texture2D(tDepth, coords));
-        #endif
-    }
+#define dQuarterThreshold 0.1
+#define dHalfThreshold 0.05
+
+float getMappedDepth(const in vec2 coords, const in vec2 selfCoords) {
+    vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
+    float d = distance(coords, selfCoords);
+    #ifdef depthTextureSupport
+        if (d > dQuarterThreshold) {
+            return texture2D(tDepthQuarter, c).r;
+        } else if (d > dHalfThreshold) {
+            return texture2D(tDepthHalf, c).r;
+        } else {
+            return texture2D(tDepth, c).r;
+        }
+    #else
+        if (d > dQuarterThreshold) {
+            return unpackRGBAToDepth(texture2D(tDepthQuarter, c));
+        } else if (d > dHalfThreshold) {
+            return unpackRGBAToDepth(texture2D(tDepthHalf, c));
+        } else {
+            return unpackRGBAToDepth(texture2D(tDepth, c));
+        }
+    #endif
 }
 
 vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {
@@ -72,6 +99,12 @@ vec3 normalFromDepth(const in float depth, const in float depth1, const in float
     return normalize(normal);
 }
 
+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);
+}
+
 // StarCraft II Ambient Occlusion by [Filion and McNaughton 2008]
 void main(void) {
     vec2 invTexSize = 1.0 / uTexSize;
@@ -95,24 +128,50 @@ void main(void) {
     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));
-    }
+    #ifdef dMultiScale
+        float pixelSize = getPixelSize(selfCoords, selfDepth);
+
+        for(int l = 0; l < dLevels; l++) {
+            // TODO: smooth transition
+            if (pixelSize * uNearThreshold > uLevelRadius[l]) continue;
+            if (pixelSize * uFarThreshold < uLevelRadius[l]) continue;
+
+            float levelOcclusion = 0.0;
+            for(int i = 0; i < dNSamples; i++) {
+                vec3 sampleViewPos = TBN * uSamples[i];
+                sampleViewPos = selfViewPos + sampleViewPos * uLevelRadius[l];
+
+                vec4 offset = vec4(sampleViewPos, 1.0);
+                offset = uProjection * offset;
+                offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
+
+                float sampleDepth = getMappedDepth(offset.xy, selfCoords);
+                float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
+
+                levelOcclusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l];
+            }
+            occlusion = max(occlusion, levelOcclusion);
+        }
+    #else
+        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 sampleDepth = getMappedDepth(offset.xy, selfCoords);
+            float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
+
+            occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
+        }
+    #endif
     occlusion = 1.0 - (uBias * occlusion / float(dNSamples));
 
     vec2 packedOcclusion = packUnitIntervalToRG(clamp(occlusion, 0.01, 1.0));

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

@@ -56,11 +56,24 @@ export class QuickStyles extends PurePluginUIComponent {
                 postprocessing: {
                     outline: {
                         name: 'on',
-                        params: { scale: 1, color: Color(0x000000), threshold: 0.25, includeTransparent: true }
+                        params: {
+                            scale: 1,
+                            color: Color(0x000000),
+                            threshold: 0.25,
+                            includeTransparent: true,
+                        }
                     },
                     occlusion: {
                         name: 'on',
-                        params: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1, color: Color(0x000000) }
+                        params: {
+                            multiScale: { name: 'off', params: {} },
+                            radius: 5,
+                            bias: 0.8,
+                            blurKernelSize: 15,
+                            samples: 32,
+                            resolutionScale: 1,
+                            color: Color(0x000000),
+                        }
                     },
                     shadow: { name: 'off', params: {} },
                 }
@@ -79,13 +92,26 @@ export class QuickStyles extends PurePluginUIComponent {
                         name: 'on',
                         params: pp.outline.name === 'on'
                             ? pp.outline.params
-                            : { scale: 1, color: Color(0x000000), threshold: 0.33, includeTransparent: true }
+                            : {
+                                scale: 1,
+                                color: Color(0x000000),
+                                threshold: 0.33,
+                                includeTransparent: true,
+                            }
                     },
                     occlusion: {
                         name: 'on',
                         params: pp.occlusion.name === 'on'
                             ? pp.occlusion.params
-                            : { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1, color: Color(0x000000) }
+                            : {
+                                multiScale: { name: 'off', params: {} },
+                                radius: 5,
+                                bias: 0.8,
+                                blurKernelSize: 15,
+                                samples: 32,
+                                resolutionScale: 1,
+                                color: Color(0x000000),
+                            }
                     },
                     shadow: { name: 'off', params: {} },
                 }

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

@@ -206,6 +206,7 @@ export const STYLIZED_POSTPROCESSING: Partial<PostprocessingProps> = {
     occlusion: {
         name: 'on' as const, params: {
             samples: 32,
+            multiScale: { name: 'off', params: {} },
             radius: 5,
             bias: 0.8,
             blurKernelSize: 15,

+ 10 - 9
src/mol-util/clip.ts

@@ -56,14 +56,14 @@ export namespace Clip {
     export type Params = typeof Params
     export type Props = PD.Values<Params>
 
-    function createClipObjects() {
+    function createClipObjects(count: number) {
         return {
             count: 0,
-            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),
+            type: (new Array(count)).fill(1),
+            invert: (new Array(count)).fill(false),
+            position: (new Array(count * 3)).fill(0),
+            rotation: (new Array(count * 4)).fill(0),
+            scale: (new Array(count * 3)).fill(1),
         };
     }
 
@@ -73,8 +73,9 @@ export namespace Clip {
     const vB = Vec3();
 
     export function getClip(props: Props, clip?: Clip): Clip {
-        const { type, invert, position, rotation, scale } = clip?.objects || createClipObjects();
-        for (let i = 0, il = props.objects.length; i < il; ++i) {
+        const count = props.objects.length;
+        const { type, invert, position, rotation, scale } = clip?.objects || createClipObjects(count);
+        for (let i = 0; i < count; ++i) {
             const p = props.objects[i];
             type[i] = Type[p.type];
             invert[i] = p.invert;
@@ -84,7 +85,7 @@ export namespace Clip {
         }
         return {
             variant: props.variant,
-            objects: { count: props.objects.length, type, invert, position, rotation, scale }
+            objects: { count, type, invert, position, rotation, scale }
         };
     }