diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ff693dc26143f04565512660bfb48933afa824..f66925d92bd3ad802b05e907c16ce05b636f3077 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Note that since we don't clearly distinguish between a public and private interf - Relative frame support for ``Canvas3D`` viewport. - Fix bug in screenshot copy UI. - Add ability to select residues from a list of identifiers to the Selection UI. +- Fix SSAO bugs when used with ``Canvas3D`` viewport. ## [v2.0.4] - 2021-04-20 diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index cb5a4468f169d8b5badb7d39dccaaf6407b7ec06..628ef8252e956e4e4f10f4bdfca6b93ae7cc3940 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -234,8 +234,8 @@ namespace Camera { up: Vec3.create(0, 1, 0), target: Vec3.create(0, 0, 0), - radius: 0, - radiusMax: 0, + radius: 10, + radiusMax: 10, fog: 50, clipFar: true }; diff --git a/src/mol-canvas3d/camera/transition.ts b/src/mol-canvas3d/camera/transition.ts index 62f3f50c5c030947329fbb637221e42d7b12893c..ad32bb076952feb14ab83939b68a02d18d69d335 100644 --- a/src/mol-canvas3d/camera/transition.ts +++ b/src/mol-canvas3d/camera/transition.ts @@ -39,6 +39,9 @@ class CameraTransitionManager { this._target.radius = this._target.radiusMax; } + if (this._target.radius < 0.01) this._target.radius = 0.01; + if (this._target.radiusMax < 0.01) this._target.radiusMax = 0.01; + if (!this.inTransition && durationMs <= 0 || (typeof to.mode !== 'undefined' && to.mode !== this.camera.state.mode)) { this.finish(this._target); return; diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 85e55a0f8b212118986c7b908a7a110a890feb8e..9887d61e372e278d71d77b9f5156887083a9f1f0 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -106,7 +106,7 @@ interface Canvas3DContext { } namespace Canvas3DContext { - const DefaultAttribs = { + export const DefaultAttribs = { /** true by default to avoid issues with Safari (Jan 2021) */ antialias: true, /** true to support multiple Canvas3D objects with a single context */ diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts index 1f671d251495688f459f0f6502e710f95b284b06..c851c8acc5d320357d56172b1978f16f5eec2268 100644 --- a/src/mol-canvas3d/passes/postprocessing.ts +++ b/src/mol-canvas3d/passes/postprocessing.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz> @@ -13,7 +13,7 @@ import { Texture } from '../../mol-gl/webgl/texture'; import { ValueCell } from '../../mol-util'; import { createComputeRenderItem } from '../../mol-gl/webgl/render-item'; import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable'; -import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra'; +import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { RenderTarget } from '../../mol-gl/webgl/render-target'; import { DrawPass } from './draw'; @@ -70,6 +70,7 @@ const SsaoSchema = { uProjection: UniformSpec('m4'), uInvProjection: UniformSpec('m4'), + uBounds: UniformSpec('v4'), uTexSize: UniformSpec('v2'), @@ -89,6 +90,7 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender uProjection: ValueCell.create(Mat4.identity()), uInvProjection: ValueCell.create(Mat4.identity()), + uBounds: ValueCell.create(Vec4()), uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)), @@ -118,6 +120,7 @@ const SsaoBlurSchema = { uNear: UniformSpec('f'), uFar: UniformSpec('f'), + uBounds: UniformSpec('v4'), dOrthographic: DefineSpec('number'), }; @@ -139,6 +142,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir uNear: ValueCell.create(0.0), uFar: ValueCell.create(10000.0), + uBounds: ValueCell.create(Vec4()), dOrthographic: ValueCell.create(0), }; @@ -286,10 +290,14 @@ export class PostprocessingPass { private readonly renderable: PostprocessingRenderable - private scale: number + private ssaoScale: number + private calcSsaoScale() { + // downscale ssao for high pixel-ratios + return Math.min(1, 1 / this.webgl.pixelRatio); + } constructor(private webgl: WebGLContext, drawPass: DrawPass) { - this.scale = 1 / this.webgl.pixelRatio; + this.ssaoScale = this.calcSsaoScale(); const { colorTarget, depthTexture } = drawPass; const width = colorTarget.getWidth(); @@ -298,7 +306,7 @@ export class PostprocessingPass { this.nSamples = 1; this.blurKernelSize = 1; - this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear'); + this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest'); this.outlinesTarget = webgl.createRenderTarget(width, height, false); this.outlinesRenderable = getOutlinesRenderable(webgl, depthTexture); @@ -317,14 +325,14 @@ export class PostprocessingPass { this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer(); this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer(); - const sw = Math.floor(width * this.scale); - const sh = Math.floor(height * this.scale); + const sw = Math.floor(width * this.ssaoScale); + const sh = Math.floor(height * this.ssaoScale); - this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); + this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); this.ssaoDepthTexture.define(sw, sh); this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0'); - this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); + this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); this.ssaoDepthBlurProxyTexture.define(sw, sh); this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0'); @@ -338,9 +346,13 @@ export class PostprocessingPass { setSize(width: number, height: number) { const [w, h] = this.renderable.values.uTexSize.ref.value; - if (width !== w || height !== h) { - const sw = Math.floor(width * this.scale); - const sh = Math.floor(height * this.scale); + const ssaoScale = this.calcSsaoScale(); + + 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.ssaoDepthTexture.define(sw, sh); @@ -349,8 +361,8 @@ export class PostprocessingPass { 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.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh)); - ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh)); - ValueCell.update(this.ssaoBlurSecondPassRenderable.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)); } } @@ -367,8 +379,22 @@ export class PostprocessingPass { Mat4.invert(invProjection, camera.projection); if (props.occlusion.name === 'on') { - ValueCell.updateIfChanged(this.ssaoRenderable.values.uProjection, camera.projection); - ValueCell.updateIfChanged(this.ssaoRenderable.values.uInvProjection, invProjection); + 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), + Math.floor(v.y * s) / (h * s), + Math.ceil((v.x + v.width) * s) / (w * s), + Math.ceil((v.y + v.height) * s) / (h * s) + ); + ValueCell.update(b, b.ref.value); + ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value); + ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value); ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near); ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near); @@ -376,7 +402,9 @@ export class PostprocessingPass { ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far); ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far); - if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateSsaoBlur = true; } + if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) { + needsUpdateSsaoBlur = true; + } ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic); ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic); @@ -384,7 +412,7 @@ export class PostprocessingPass { needsUpdateSsao = true; this.nSamples = props.occlusion.params.samples; - ValueCell.updateIfChanged(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples)); + ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples)); ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples); } ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius)); @@ -394,10 +422,10 @@ export class PostprocessingPass { needsUpdateSsaoBlur = true; this.blurKernelSize = props.occlusion.params.blurKernelSize; - let kernel = getBlurKernel(this.blurKernelSize); + const kernel = getBlurKernel(this.blurKernelSize); - ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel); - ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel); + ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel); + ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel); ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize); ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize); } @@ -467,10 +495,10 @@ export class PostprocessingPass { if (props.occlusion.name === 'on') { const { x, y, width, height } = camera.viewport; - const sx = Math.floor(x * this.scale); - const sy = Math.floor(y * this.scale); - const sw = Math.floor(width * this.scale); - const sh = Math.floor(height * this.scale); + const sx = Math.floor(x * this.ssaoScale); + const sy = Math.floor(y * this.ssaoScale); + const sw = Math.ceil(width * this.ssaoScale); + const sh = Math.ceil(height * this.ssaoScale); this.webgl.gl.viewport(sx, sy, sw, sh); this.webgl.gl.scissor(sx, sy, sw, sh); diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts index b7872bc3ecc644daf819e19ab9a99c4eeb652596..55cdf3062610ad8c22c2af544991c84fc7172eeb 100644 --- a/src/mol-gl/shader/postprocessing.frag.ts +++ b/src/mol-gl/shader/postprocessing.frag.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz> @@ -88,7 +88,8 @@ float getSsao(vec2 coords) { } else if (rawSsao > 0.001) { return rawSsao; } - return 0.0; + // treat values close to 0.0 as errors and return no occlusion + return 1.0; } void main(void) { diff --git a/src/mol-gl/shader/ssao-blur.frag.ts b/src/mol-gl/shader/ssao-blur.frag.ts index 34d16aa7e5719a8a536480321f8acd32137b1bdf..efa894dc21f8d9e4f490424f1bb5afd6e9864a57 100644 --- a/src/mol-gl/shader/ssao-blur.frag.ts +++ b/src/mol-gl/shader/ssao-blur.frag.ts @@ -1,7 +1,8 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz> + * @author Alexander Rose <alexander.rose@weirdbyte.de> */ export const ssaoBlur_frag = ` @@ -11,6 +12,7 @@ precision highp sampler2D; uniform sampler2D tSsaoDepth; uniform vec2 uTexSize; +uniform vec4 uBounds; uniform float uKernel[dOcclusionKernelSize]; @@ -36,16 +38,25 @@ 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; +} + void main(void) { vec2 coords = gl_FragCoord.xy / uTexSize; vec2 packedDepth = texture2D(tSsaoDepth, coords).zw; + if (outsideBounds(coords)) { + gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth); + return; + } + float selfDepth = unpackRGToUnitInterval(packedDepth); // if background and if second pass if (isBackground(selfDepth) && uBlurDirectionY != 0.0) { - gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth); - return; + gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth); + return; } float selfViewZ = getViewZ(selfDepth); @@ -57,6 +68,9 @@ void main(void) { // only if kernelSize is odd for (int i = -dOcclusionKernelSize / 2; i <= dOcclusionKernelSize / 2; i++) { vec2 sampleCoords = coords + float(i) * offset; + if (outsideBounds(sampleCoords)) { + continue; + } vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords); diff --git a/src/mol-gl/shader/ssao.frag.ts b/src/mol-gl/shader/ssao.frag.ts index 5b36d9116b6e6566a0d9a60ed2ab6005d6992da6..029661de7af06a9a8c4d5599dfb599b737efdf7a 100644 --- a/src/mol-gl/shader/ssao.frag.ts +++ b/src/mol-gl/shader/ssao.frag.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz> @@ -13,14 +13,14 @@ precision highp sampler2D; #include common uniform sampler2D tDepth; +uniform vec2 uTexSize; +uniform vec4 uBounds; uniform vec3 uSamples[dNSamples]; uniform mat4 uProjection; uniform mat4 uInvProjection; -uniform vec2 uTexSize; - uniform float uRadius; uniform float uBias; @@ -46,8 +46,12 @@ 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) { - return unpackRGBAToDepth(texture2D(tDepth, coords)); + return outsideBounds(coords) ? 1.0 : unpackRGBAToDepth(texture2D(tDepth, coords)); } vec3 normalFromDepth(const in float depth, const in float depth1, const in float depth2, vec2 offset1, vec2 offset2) {