diff --git a/src/apps/docking-viewer/viewport.tsx b/src/apps/docking-viewer/viewport.tsx index fe92d0f9623a4583c52d17f832faa02575cec632..658c3278aba02681fc3195057547eb905cc19eb3 100644 --- a/src/apps/docking-viewer/viewport.tsx +++ b/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), diff --git a/src/examples/lighting/index.ts b/src/examples/lighting/index.ts index ceab47858abd78c4b5e964b4dcbd64a8563989a3..fb7b182d2d88ca1ccb04a6adeaf0405ff744f9d0 100644 --- a/src/examples/lighting/index.ts +++ b/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, diff --git a/src/extensions/cellpack/model.ts b/src/extensions/cellpack/model.ts index e58388495eeab24a682abeb7712f09817c7a3f12..6ea5dbd2873b53a2468451e841ee1f5677dd033b 100644 --- a/src/extensions/cellpack/model.ts +++ b/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, diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts index efc98c0f4987c1d2054df1c2eb3b6e0d420e0beb..2d130e622382e47667abf8fa66d233eafaee5bcb 100644 --- a/src/mol-canvas3d/passes/postprocessing.ts +++ b/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,26 @@ 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 { radius, bias } = levels || { + radius: (new Array(5 * 3)).fill(0), + bias: (new Array(5 * 3)).fill(0), + }; + props = props.slice().sort((a, b) => a.radius - b.radius); + for (let i = 0, il = props.length; i < il; ++i) { + const p = props[i]; + radius[i] = Math.pow(2, p.radius); + bias[i] = p.bias; + } + return { count: props.length, 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 +448,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 +473,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 +487,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 +505,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 +536,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 +551,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 +639,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 +685,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 +867,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 +998,3 @@ export class AntialiasingPass { } } } - diff --git a/src/mol-gl/shader/ssao.frag.ts b/src/mol-gl/shader/ssao.frag.ts index b4d61f5dbd1614f36f6d00207da855e1bcf25e83..ac851e47fa8bf28a56dc78e379c5559c90c07dcb 100644 --- a/src/mol-gl/shader/ssao.frag.ts +++ b/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)); diff --git a/src/mol-plugin-ui/structure/quick-styles.tsx b/src/mol-plugin-ui/structure/quick-styles.tsx index 2e93764dac2e1842724041a718f819f204ac4d34..29b0115286497a91447e4dc3e914b6bd23eb1e81 100644 --- a/src/mol-plugin-ui/structure/quick-styles.tsx +++ b/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: {} }, } diff --git a/src/mol-plugin/util/headless-screenshot.ts b/src/mol-plugin/util/headless-screenshot.ts index 26948caa0c5df5f09664a914cc3783fa14d69af5..668b48899770ee3063808e8a45a1c6199fb40739 100644 --- a/src/mol-plugin/util/headless-screenshot.ts +++ b/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,