Browse Source

Merge pull request #637 from molstar/sss

Screen-space shadows
Alexander Rose 2 years ago
parent
commit
7c394525c1

+ 2 - 0
CHANGELOG.md

@@ -7,6 +7,8 @@ Note that since we don't clearly distinguish between a public and private interf
 ## [Unreleased]
 - Excluded common protein caps `NME` and `ACE` from the ligand selection query
 
+- Add screen-space shadow post-processing effect
+
 ## [v3.25.1] - 2022-11-20
 
 - Fix edge-case in `Structure.eachUnitPair` with single-element units

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

@@ -31,7 +31,8 @@ function shinyStyle(plugin: PluginContext) {
         postprocessing: {
             ...plugin.canvas3d!.props.postprocessing,
             occlusion: { name: 'off', params: {} },
-            outline: { name: 'off', params: {} }
+            shadow: { name: 'off', params: {} },
+            outline: { name: 'off', params: {} },
         }
     } });
 }
@@ -48,13 +49,14 @@ function occlusionStyle(plugin: PluginContext) {
                 blurKernelSize: 15,
                 radius: 5,
                 samples: 32,
-                resolutionScale: 1
+                resolutionScale: 1,
             } },
             outline: { name: 'on', params: {
                 scale: 1.0,
                 threshold: 0.33,
                 color: Color(0x0000),
-            } }
+            } },
+            shadow: { name: 'off', params: {} },
         }
     } });
 }

+ 7 - 4
src/examples/lighting/index.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -25,7 +25,8 @@ 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) } },
+                shadow: { name: 'off', params: {} },
             },
             renderer: {
                 ambientIntensity: 1.0,
@@ -37,7 +38,8 @@ const Canvas3DPresets = {
         canvas3d: <Preset>{
             postprocessing: {
                 occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
-                outline: { name: 'off', params: {} }
+                outline: { name: 'off', params: {} },
+                shadow: { name: 'off', params: {} },
             },
             renderer: {
                 ambientIntensity: 0.4,
@@ -50,7 +52,8 @@ const Canvas3DPresets = {
         canvas3d: <Preset>{
             postprocessing: {
                 occlusion: { name: 'off', params: {} },
-                outline: { name: 'off', params: {} }
+                outline: { name: 'off', params: {} },
+                shadow: { name: 'off', params: {} },
             },
             renderer: {
                 ambientIntensity: 0.4,

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

@@ -606,6 +606,15 @@ export const LoadCellPackModel = StateAction.build({
                         resolutionScale: 1,
                     }
                 },
+                shadow: {
+                    name: 'on',
+                    params: {
+                        bias: 0.6,
+                        maxDistance: 80,
+                        steps: 3,
+                        tolerance: 1.0,
+                    }
+                },
                 outline: {
                     name: 'on',
                     params: {

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

@@ -150,7 +150,7 @@ export class DrawPass {
                 }
             }
 
-            this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
+            this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
         }
 
         this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
@@ -204,7 +204,7 @@ export class DrawPass {
                 }
             }
 
-            this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
+            this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
         }
 
         // render transparent primitives and volumes
@@ -268,7 +268,7 @@ export class DrawPass {
                     }
                 }
 
-                this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
+                this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
 
                 if (!this.packedDepth) {
                     this.depthTextureOpaque.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');

+ 140 - 8
src/mol-canvas3d/passes/postprocessing.ts

@@ -3,6 +3,7 @@
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
  */
 
 import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
@@ -30,6 +31,8 @@ import { SmaaParams, SmaaPass } from './smaa';
 import { isTimingMode } from '../../mol-util/debug';
 import { BackgroundParams, BackgroundPass } from './background';
 import { AssetManager } from '../../mol-util/assets';
+import { Light } from '../../mol-gl/renderer';
+import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
 
 const OutlinesSchema = {
     ...QuadSchema,
@@ -69,6 +72,64 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
     return createComputeRenderable(renderItem, values);
 }
 
+const ShadowsSchema = {
+    ...QuadSchema,
+    tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+
+    uProjection: UniformSpec('m4'),
+    uInvProjection: UniformSpec('m4'),
+    uBounds: UniformSpec('v4'),
+
+    dOrthographic: DefineSpec('number'),
+    uNear: UniformSpec('f'),
+    uFar: UniformSpec('f'),
+
+    dSteps: DefineSpec('number'),
+    uMaxDistance: UniformSpec('f'),
+    uTolerance: UniformSpec('f'),
+    uBias: UniformSpec('f'),
+
+    uLightDirection: UniformSpec('v3[]'),
+    uLightColor: UniformSpec('v3[]'),
+    dLightCount: DefineSpec('number'),
+};
+type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
+
+function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
+    const width = depthTexture.getWidth();
+    const height = depthTexture.getHeight();
+
+    const values: Values<typeof ShadowsSchema> = {
+        ...QuadValues,
+        tDepth: ValueCell.create(depthTexture),
+        uTexSize: ValueCell.create(Vec2.create(width, height)),
+
+        uProjection: ValueCell.create(Mat4.identity()),
+        uInvProjection: ValueCell.create(Mat4.identity()),
+        uBounds: ValueCell.create(Vec4()),
+
+        dOrthographic: ValueCell.create(0),
+        uNear: ValueCell.create(1),
+        uFar: ValueCell.create(10000),
+
+        dSteps: ValueCell.create(1),
+        uMaxDistance: ValueCell.create(3.0),
+        uTolerance: ValueCell.create(1.0),
+        uBias: ValueCell.create(0.6),
+
+        uLightDirection: ValueCell.create([]),
+        uLightColor: ValueCell.create([]),
+        dLightCount: ValueCell.create(0),
+    };
+
+    const schema = { ...ShadowsSchema };
+    const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
+    const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
 const SsaoSchema = {
     ...QuadSchema,
     tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
@@ -204,6 +265,7 @@ const PostprocessingSchema = {
     tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    tShadows: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
     uTexSize: UniformSpec('v2'),
 
@@ -221,19 +283,22 @@ const PostprocessingSchema = {
     dOcclusionEnable: DefineSpec('boolean'),
     uOcclusionOffset: UniformSpec('v2'),
 
+    dShadowEnable: DefineSpec('boolean'),
+
     dOutlineEnable: DefineSpec('boolean'),
     dOutlineScale: DefineSpec('number'),
     uOutlineThreshold: UniformSpec('f'),
 };
 type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
 
-function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture): PostprocessingRenderable {
+function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture): PostprocessingRenderable {
     const values: Values<typeof PostprocessingSchema> = {
         ...QuadValues,
         tSsaoDepth: ValueCell.create(ssaoDepthTexture),
         tColor: ValueCell.create(colorTexture),
         tDepthOpaque: ValueCell.create(depthTextureOpaque),
         tDepthTransparent: ValueCell.create(depthTextureTransparent),
+        tShadows: ValueCell.create(shadowsTexture),
         tOutlines: ValueCell.create(outlinesTexture),
         uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
 
@@ -251,6 +316,8 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
         dOcclusionEnable: ValueCell.create(true),
         uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
 
+        dShadowEnable: ValueCell.create(false),
+
         dOutlineEnable: ValueCell.create(false),
         dOutlineScale: ValueCell.create(1),
         uOutlineThreshold: ValueCell.create(0.33),
@@ -274,6 +341,15 @@ export const PostprocessingParams = {
         }),
         off: PD.Group({})
     }, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
+    shadow: PD.MappedStatic('off', {
+        on: PD.Group({
+            steps: PD.Numeric(1, { min: 1, max: 20, step: 1 }),
+            bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
+            maxDistance: PD.Numeric(3.0, { min: 0.0, max: 100.0, step: 1.0 }),
+            tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
+        }),
+        off: PD.Group({})
+    }, { cycle: true, description: 'Simplistic shadows' }),
     outline: PD.MappedStatic('off', {
         on: PD.Group({
             scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
@@ -289,11 +365,12 @@ export const PostprocessingParams = {
     }, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
     background: PD.Group(BackgroundParams, { isFlat: true }),
 };
+
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
 export class PostprocessingPass {
     static isEnabled(props: PostprocessingProps) {
-        return props.occlusion.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
+        return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
     }
 
     static isOutlineEnabled(props: PostprocessingProps) {
@@ -305,6 +382,9 @@ export class PostprocessingPass {
     private readonly outlinesTarget: RenderTarget;
     private readonly outlinesRenderable: OutlinesRenderable;
 
+    private readonly shadowsTarget: RenderTarget;
+    private readonly shadowsRenderable: ShadowsRenderable;
+
     private readonly ssaoFramebuffer: Framebuffer;
     private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
     private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
@@ -350,6 +430,9 @@ export class PostprocessingPass {
         this.outlinesTarget = webgl.createRenderTarget(width, height, false);
         this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent);
 
+        this.shadowsTarget = webgl.createRenderTarget(width, height, false);
+        this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
+
         this.ssaoFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
@@ -373,7 +456,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.outlinesTarget.texture, this.ssaoDepthTexture);
+        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture);
 
         this.background = new BackgroundPass(webgl, assetManager, width, height);
     }
@@ -389,12 +472,14 @@ export class PostprocessingPass {
             const sh = Math.floor(height * this.ssaoScale);
             this.target.setSize(width, height);
             this.outlinesTarget.setSize(width, height);
+            this.shadowsTarget.setSize(width, height);
             this.downsampledDepthTarget.setSize(sw, sh);
             this.ssaoDepthTexture.define(sw, sh);
             this.ssaoDepthBlurProxyTexture.define(sw, sh);
 
             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.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));
@@ -404,25 +489,28 @@ export class PostprocessingPass {
         }
     }
 
-    private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
+    private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
+        let needsUpdateShadows = false;
         let needsUpdateMain = false;
         let needsUpdateSsao = false;
         let needsUpdateSsaoBlur = false;
 
         const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
         const outlinesEnabled = props.outline.name === 'on';
+        const shadowsEnabled = props.shadow.name === 'on';
         const occlusionEnabled = props.occlusion.name === 'on';
 
         const invProjection = Mat4.identity();
         Mat4.invert(invProjection, camera.projection);
 
+        const [w, h] = this.renderable.values.uTexSize.ref.value;
+        const v = camera.viewport;
+
         if (props.occlusion.name === 'on') {
             ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
             ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
 
-            const [w, h] = this.renderable.values.uTexSize.ref.value;
             const b = this.ssaoRenderable.values.uBounds;
-            const v = camera.viewport;
             const s = this.ssaoScale;
             Vec4.set(b.ref.value,
                 Math.floor(v.x * s) / (w * s),
@@ -494,6 +582,38 @@ export class PostprocessingPass {
             }
         }
 
+        if (props.shadow.name === 'on') {
+            ValueCell.update(this.shadowsRenderable.values.uProjection, camera.projection);
+            ValueCell.update(this.shadowsRenderable.values.uInvProjection, invProjection);
+
+            Vec4.set(this.shadowsRenderable.values.uBounds.ref.value,
+                v.x / w,
+                v.y / h,
+                (v.x + v.width) / w,
+                (v.y + v.height) / h
+            );
+            ValueCell.update(this.shadowsRenderable.values.uBounds, this.shadowsRenderable.values.uBounds.ref.value);
+
+            ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
+            ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
+            ValueCell.updateIfChanged(this.shadowsRenderable.values.dOrthographic, orthographic);
+
+            ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
+            ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
+            ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.shadow.params.bias);
+            if (this.shadowsRenderable.values.dSteps.ref.value !== props.shadow.params.steps) {
+                ValueCell.update(this.shadowsRenderable.values.dSteps, props.shadow.params.steps);
+                needsUpdateShadows = true;
+            }
+
+            ValueCell.update(this.shadowsRenderable.values.uLightDirection, light.direction);
+            ValueCell.update(this.shadowsRenderable.values.uLightColor, light.color);
+            if (this.shadowsRenderable.values.dLightCount.ref.value !== light.count) {
+                ValueCell.update(this.shadowsRenderable.values.dLightCount, light.count);
+                needsUpdateShadows = true;
+            }
+        }
+
         if (props.outline.name === 'on') {
             let { threshold } = props.outline.params;
             // orthographic needs lower threshold
@@ -522,11 +642,18 @@ export class PostprocessingPass {
         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 (needsUpdateShadows) {
+            this.shadowsRenderable.update();
+        }
+
         if (needsUpdateSsao) {
             this.ssaoRenderable.update();
         }
@@ -564,15 +691,20 @@ export class PostprocessingPass {
         this.transparentBackground = value;
     }
 
-    render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
+    render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
         if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
-        this.updateState(camera, transparentBackground, backgroundColor, props);
+        this.updateState(camera, transparentBackground, backgroundColor, props, light);
 
         if (props.outline.name === 'on') {
             this.outlinesTarget.bind();
             this.outlinesRenderable.render();
         }
 
+        if (props.shadow.name === 'on') {
+            this.shadowsTarget.bind();
+            this.shadowsRenderable.render();
+        }
+
         // don't render occlusion if offset is given,
         // which will reuse the existing occlusion
         if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) {

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

@@ -54,6 +54,7 @@ export const enum MarkingType {
 interface Renderer {
     readonly stats: RendererStats
     readonly props: Readonly<RendererProps>
+    readonly light: Readonly<Light>
 
     clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean) => void
     clearDepth: (packed?: boolean) => void
@@ -103,13 +104,13 @@ export const RendererParams = {
     xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
 
     light: PD.ObjectList({
-        inclination: PD.Numeric(180, { min: 0, max: 180, step: 1 }),
-        azimuth: PD.Numeric(0, { min: 0, max: 360, step: 1 }),
+        inclination: PD.Numeric(150, { min: 0, max: 180, step: 1 }),
+        azimuth: PD.Numeric(320, { min: 0, max: 360, step: 1 }),
         color: PD.Color(Color.fromNormalizedRgb(1.0, 1.0, 1.0)),
         intensity: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
     }, o => Color.toHexString(o.color), { defaultValue: [{
-        inclination: 180,
-        azimuth: 0,
+        inclination: 150,
+        azimuth: 320,
         color: Color.fromNormalizedRgb(1.0, 1.0, 1.0),
         intensity: 0.6
     }] }),
@@ -118,7 +119,7 @@ export const RendererParams = {
 };
 export type RendererProps = PD.Values<typeof RendererParams>
 
-type Light = {
+export type Light = {
     count: number
     direction: number[]
     color: number[]
@@ -827,6 +828,9 @@ namespace Renderer {
                     instancedDrawCount: stats.instancedDrawCount,
                 };
             },
+            get light(): Light {
+                return light;
+            },
             dispose: () => {
                 // TODO
             }

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

@@ -14,6 +14,7 @@ uniform sampler2D tSsaoDepth;
 uniform sampler2D tColor;
 uniform sampler2D tDepthOpaque;
 uniform sampler2D tDepthTransparent;
+uniform sampler2D tShadows;
 uniform sampler2D tOutlines;
 uniform vec2 uTexSize;
 
@@ -120,7 +121,20 @@ void main(void) {
         }
     #endif
 
-    // outline needs to be handled after occlusion to keep them clean
+    #ifdef dShadowEnable
+        if (!isBackground(opaqueDepth)) {
+            viewDist = abs(getViewZ(opaqueDepth));
+            fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
+            vec4 shadow = texture2D(tShadows, coords);
+            if (!uTransparentBackground) {
+                color.rgb = mix(mix(vec3(0), uFogColor, fogFactor), color.rgb, shadow.a);
+            } else {
+                color.rgb = mix(vec3(0) * (1.0 - fogFactor), color.rgb, shadow.a);
+            }
+        }
+    #endif
+
+    // outline needs to be handled after occlusion and shadow to keep them clean
     #ifdef dOutlineEnable
         float closestTexel;
         float outline = getOutline(coords, opaqueDepth, closestTexel);

+ 131 - 0
src/mol-gl/shader/shadows.frag.ts

@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Ludovic Autin <ludovic.autin@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export const shadows_frag = `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+#include common
+
+uniform sampler2D tDepth;
+uniform vec2 uTexSize;
+uniform vec4 uBounds;
+
+uniform float uNear;
+uniform float uFar;
+
+#if dLightCount != 0
+    uniform vec3 uLightDirection[dLightCount];
+    uniform vec3 uLightColor[dLightCount];
+#endif
+
+uniform mat4 uProjection;
+uniform mat4 uInvProjection;
+
+uniform float uMaxDistance;
+uniform float uTolerance;
+uniform float uBias;
+
+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 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) {
+    #ifdef depthTextureSupport
+        return texture2D(tDepth, coords).r;
+    #else
+        return unpackRGBAToDepth(texture2D(tDepth, coords));
+    #endif
+}
+
+float screenFade(const in vec2 coords) {
+    vec2 c = (coords - uBounds.xy) / (uBounds.zw - uBounds.xy);
+    vec2 fade = max(12.0 * abs(c - 0.5) - 5.0, vec2(0.0));
+    return saturate(1.0 - dot(fade, fade));
+}
+
+// based on https://panoskarabelas.com/posts/screen_space_shadows/
+float screenSpaceShadow(const in vec3 position, const in vec3 lightDirection, const in float stepLength) {
+    // Ray position and direction (in view-space)
+    vec3 rayPos = position;
+    vec3 rayDir = -lightDirection;
+
+    // Compute ray step
+    vec3 rayStep = rayDir * stepLength;
+
+    // Ray march towards the light
+    float occlusion = 0.0;
+    vec4 rayCoords = vec4(0.0);
+    for (int i = 0; i < dSteps; ++i) {
+        // Step the ray
+        rayPos += rayStep;
+
+        rayCoords = uProjection * vec4(rayPos, 1.0);
+        rayCoords.xyz = (rayCoords.xyz / rayCoords.w) * 0.5 + 0.5;
+
+        if (outsideBounds(rayCoords.xy))
+            return 1.0;
+
+        // Compute the difference between the ray's and the camera's depth
+        float depth = getDepth(rayCoords.xy);
+        float viewZ = getViewZ(depth);
+        float zDelta = rayPos.z - viewZ;
+
+        if (zDelta < uTolerance) {
+            occlusion = 1.0;
+
+            // Fade out as we approach the edges of the screen
+            occlusion *= screenFade(rayCoords.xy);
+
+            break;
+        }
+    }
+
+    return 1.0 - (uBias * occlusion);
+}
+
+void main(void) {
+    vec2 invTexSize = 1.0 / uTexSize;
+    vec2 selfCoords = gl_FragCoord.xy * invTexSize;
+
+    float selfDepth = getDepth(selfCoords);
+
+    if (isBackground(selfDepth)) {
+        gl_FragColor = vec4(0.0);
+        return;
+    }
+
+    vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
+    float stepLength = uMaxDistance / float(dSteps);
+
+    float o = 1.0;
+    #if dLightCount != 0
+        float sh[dLightCount];
+        #pragma unroll_loop_start
+        for (int i = 0; i < dLightCount; ++i) {
+            sh[i] = screenSpaceShadow(selfViewPos, uLightDirection[i], stepLength);
+            o = min(o, sh[i]);
+        }
+        #pragma unroll_loop_end
+    #endif
+
+    gl_FragColor = vec4(o);
+}
+`;

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

@@ -62,6 +62,7 @@ export class QuickStyles extends PurePluginUIComponent {
                         name: 'on',
                         params: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1 }
                     },
+                    shadow: { name: 'off', params: {} },
                 }
             });
         }
@@ -86,6 +87,7 @@ export class QuickStyles extends PurePluginUIComponent {
                             ? pp.occlusion.params
                             : { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, resolutionScale: 1 }
                     },
+                    shadow: { name: 'off', params: {} },
                 }
             });
         }

+ 3 - 0
src/mol-plugin-ui/viewport/simple-settings.tsx

@@ -59,6 +59,7 @@ const SimpleSettingsParams = {
     }, { pivot: 'color' }),
     lighting: PD.Group({
         occlusion: Canvas3DParams.postprocessing.params.occlusion,
+        shadow: Canvas3DParams.postprocessing.params.shadow,
         outline: Canvas3DParams.postprocessing.params.outline,
         fog: Canvas3DParams.cameraFog,
     }, { isFlat: true }),
@@ -114,6 +115,7 @@ const SimpleSettingsMapping = ParamMapping({
             },
             lighting: {
                 occlusion: canvas.postprocessing.occlusion,
+                shadow: canvas.postprocessing.shadow,
                 outline: canvas.postprocessing.outline,
                 fog: canvas.cameraFog,
             },
@@ -129,6 +131,7 @@ const SimpleSettingsMapping = ParamMapping({
         canvas.transparentBackground = s.background.transparent;
         canvas.renderer.backgroundColor = s.background.color;
         canvas.postprocessing.occlusion = s.lighting.occlusion;
+        canvas.postprocessing.shadow = s.lighting.shadow;
         canvas.postprocessing.outline = s.lighting.outline;
         canvas.postprocessing.background = s.background.style;
         canvas.cameraFog = s.lighting.fog;