diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea385793efcac96000e292b2bb3de5a9d3ebb8d..c7fe9bc5cd3f1b35b83059e94ac4d97fd7fddfd3 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/src/apps/docking-viewer/viewport.tsx b/src/apps/docking-viewer/viewport.tsx index 67976ac1ef065cd73fc79f58c8e1e57bb263d487..ba5ce4598a9920a917064968ad97f754706cad07 100644 --- a/src/apps/docking-viewer/viewport.tsx +++ b/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: {} }, } } }); } diff --git a/src/examples/lighting/index.ts b/src/examples/lighting/index.ts index 23b254b5e6065f8df1fcf3fd9403db118020d01b..a71bff56aa4c0d39b8c03f6317bd7864b7b0485a 100644 --- a/src/examples/lighting/index.ts +++ b/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, diff --git a/src/extensions/cellpack/model.ts b/src/extensions/cellpack/model.ts index 80fb0b3d979a76bff36e18de6724ace45748852c..9c62408af02efabdaaa3bf78a4b9e058892bfa9b 100644 --- a/src/extensions/cellpack/model.ts +++ b/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: { diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts index 34282e8408d16a0c59251178c6ab14843914d707..1f6c5549bc2169e74b09f6246446930781db242b 100644 --- a/src/mol-canvas3d/passes/draw.ts +++ b/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'); diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts index 591517976863a47013460eb3158cfeeb699619a4..ab2b06dad0aadb4e9af05d4c67ff6554d836773b 100644 --- a/src/mol-canvas3d/passes/postprocessing.ts +++ b/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) { diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index 086f6db4086bed82767ccb56c8d8bb7bec60f58a..c41bf3283e32076853c8e4923fa59702b0364b87 100644 --- a/src/mol-gl/renderer.ts +++ b/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 } diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts index df1acfad5791cdea0c7aed615751455ebd1cb7c8..d41cb9449bea753a2ea3d4f35421acd938d9272d 100644 --- a/src/mol-gl/shader/postprocessing.frag.ts +++ b/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); diff --git a/src/mol-gl/shader/shadows.frag.ts b/src/mol-gl/shader/shadows.frag.ts new file mode 100644 index 0000000000000000000000000000000000000000..3bdb662601b7f2188a76f1e96a6dc429b4f6fbd7 --- /dev/null +++ b/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); +} +`; \ No newline at end of file diff --git a/src/mol-plugin-ui/structure/quick-styles.tsx b/src/mol-plugin-ui/structure/quick-styles.tsx index a0c535acfd2b744d2748585c7a8db80febf23cde..ef3078b037793844447e996145b719dd4b85586b 100644 --- a/src/mol-plugin-ui/structure/quick-styles.tsx +++ b/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: {} }, } }); } diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx index a3f7eb6d939a26fb0db47839521823779989c94f..ddf7fcf54304c5503f428ec8b31c42e488a832e8 100644 --- a/src/mol-plugin-ui/viewport/simple-settings.tsx +++ b/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;