diff --git a/CHANGELOG.md b/CHANGELOG.md index 035b1b0a6421f0bdb48a7f280e64b5d6e119ad9b..a0b3c1390c5c8aa6eeb43ba74a3e40a66a5bcb10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Note that since we don't clearly distinguish between a public and private interf - Fix parsing contour-level from emdb v3 header files - Fix invalid CSS (#376) - Fix "texture not renderable" & "texture not bound" warnings (#319) +- Fix visual for bonds between two aromatic rings +- Fix visual for delocalized bonds (parsed from mmcif and mol2) +- Fix ring computation algorithm +- Add ``UnitResonance`` property with info about delocalized triplets - Resolve marking in main renderer loop to improve overall performance - Use ``throttleTime`` instead of ``debounceTime`` in sequence viewer for better responsiveness - Change line geometry default ``scaleFactor`` to 2 (3 is too big after fixing line rendering) @@ -19,6 +23,9 @@ Note that since we don't clearly distinguish between a public and private interf - Don't show 'inter-bond' and 'element-cross' visuals in line representations of polymerAndLigand preset - Fix additional mononucleotides detected as polymer components - Fix and improve ``canRemap`` handling in ``IntraUnitBonds`` +- Reuse occlusion for secondary passes during multi-sampling +- Check if marking passes are needed before doing them +- Add ``scaleFactor`` parameter to adjust resolution of occlusion calculation ## [v3.2.0] - 2022-02-17 diff --git a/docs/interesting-pdb-entries.md b/docs/interesting-pdb-entries.md index a1323f898887e0ea8320dcc240c0c612ded816e4..275f177905696c88369e532251757cba4aaffa70 100644 --- a/docs/interesting-pdb-entries.md +++ b/docs/interesting-pdb-entries.md @@ -34,6 +34,14 @@ * ACE (many, e.g. 5AGU, 1E1X) * ACY in 7ABY * NH2 (many, e.g. 6Y13) +* Ligands with many rings + * STU (e.g. 1U59) - many fused rings + * HT (e.g. 127D) - rings connected by a single bond + * J2C (e.g. 7EFJ) - rings connected by a single atom + * RBF (e.g. 7QF2) - three linearly fused rings + * TA1 (e.g. 1JFF) - many fused rings (incl. a 8-member rings) + * BPA (e.g. 1JDG) - many fused rings + * CLR (e.g. 3GKI) - four fused rings Assembly symmetries * 5M30 (Assembly 1, C3 local and pseudo) diff --git a/src/apps/docking-viewer/viewport.tsx b/src/apps/docking-viewer/viewport.tsx index d34d58701f1d60761e71a40834daa7c4e0b0c042..b988b49e3e3dcf1ae7c89fec944e04bd445dfcfa 100644 --- a/src/apps/docking-viewer/viewport.tsx +++ b/src/apps/docking-viewer/viewport.tsx @@ -44,10 +44,11 @@ function occlusionStyle(plugin: PluginContext) { postprocessing: { ...plugin.canvas3d!.props.postprocessing, occlusion: { name: 'on', params: { - samples: 64, - radius: 8, - bias: 1.0, - blurKernelSize: 13 + bias: 0.8, + blurKernelSize: 15, + radius: 5, + samples: 32, + scaleFactor: 1 } }, outline: { name: 'on', params: { scale: 1.0, diff --git a/src/examples/lighting/index.ts b/src/examples/lighting/index.ts index c296b1c8fa8505b5a561a1cc9b6eaaf6a2a036a9..97b2db364e45c6e590f48684f7749207bc674057 100644 --- a/src/examples/lighting/index.ts +++ b/src/examples/lighting/index.ts @@ -24,7 +24,7 @@ const Canvas3DPresets = { illustrative: { canvas3d: <Preset>{ postprocessing: { - occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } }, + occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, scaleFactor: 1 } }, outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000) } } }, renderer: { @@ -36,7 +36,7 @@ const Canvas3DPresets = { occlusion: { canvas3d: <Preset>{ postprocessing: { - occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15 } }, + occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, scaleFactor: 1 } }, outline: { name: 'off', params: {} } }, renderer: { diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts index b32caf23453b1ec7250c0e1733627fa15c39bae1..469393ad983b4bf1b0aacd1532fe37f1b9fda577 100644 --- a/src/mol-canvas3d/passes/draw.ts +++ b/src/mol-canvas3d/passes/draw.ts @@ -307,19 +307,22 @@ export class DrawPass { } if (markingEnabled) { - const markingDepthTest = props.marking.ghostEdgeStrength < 1; - if (markingDepthTest) { - this.marking.depthTarget.bind(); + const markerAverage = scene.getMarkerAverage(); + if (markerAverage > 0) { + const markingDepthTest = props.marking.ghostEdgeStrength < 1; + if (markingDepthTest && markerAverage !== 1) { + this.marking.depthTarget.bind(); + renderer.clear(false, true); + renderer.renderMarkingDepth(scene.primitives, camera, null); + } + + this.marking.maskTarget.bind(); renderer.clear(false, true); - renderer.renderMarkingDepth(scene.primitives, camera, null); - } - - this.marking.maskTarget.bind(); - renderer.clear(false, true); - renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null); + renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null); - this.marking.update(props.marking); - this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget); + this.marking.update(props.marking); + this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget); + } } if (helper.debug.isEnabled) { diff --git a/src/mol-canvas3d/passes/multi-sample.ts b/src/mol-canvas3d/passes/multi-sample.ts index dba08fb53c10aa61c3d792bbb743ed2b7197747d..6f7ca4358d657622ad81bbd9186d28857622d6f4 100644 --- a/src/mol-canvas3d/passes/multi-sample.ts +++ b/src/mol-canvas3d/passes/multi-sample.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> */ @@ -157,6 +157,14 @@ export class MultiSamplePass { ValueCell.update(compose.values.uWeight, sampleWeight); // render scene + if (i === 0) { + drawPass.postprocessing.setOcclusionOffset(0, 0); + } else { + drawPass.postprocessing.setOcclusionOffset( + offset[0] / width, + offset[1] / height + ); + } drawPass.render(ctx, props, false); // compose rendered scene with compose target @@ -175,6 +183,8 @@ export class MultiSamplePass { compose.render(); } + drawPass.postprocessing.setOcclusionOffset(0, 0); + ValueCell.update(compose.values.uWeight, 1.0); ValueCell.update(compose.values.tColor, composeTarget.texture); compose.update(); @@ -236,6 +246,14 @@ export class MultiSamplePass { camera.update(); // render scene + if (sampleIndex === 0) { + drawPass.postprocessing.setOcclusionOffset(0, 0); + } else { + drawPass.postprocessing.setOcclusionOffset( + offset[0] / width, + offset[1] / height + ); + } drawPass.render(ctx, props, false); // compose rendered scene with compose target @@ -258,6 +276,8 @@ export class MultiSamplePass { } } + drawPass.postprocessing.setOcclusionOffset(0, 0); + this.bindOutputTarget(toDrawingBuffer); gl.viewport(x, y, width, height); gl.scissor(x, y, width, height); @@ -291,23 +311,23 @@ const JitterVectors = [ [0, 0] ], [ - [4, 4], [-4, -4] + [0, 0], [-4, -4] ], [ - [-2, -6], [6, -2], [-6, 2], [2, 6] + [0, 0], [6, -2], [-6, 2], [2, 6] ], [ - [1, -3], [-1, 3], [5, 1], [-3, -5], + [0, 0], [-1, 3], [5, 1], [-3, -5], [-5, 5], [-7, -1], [3, 7], [7, -7] ], [ - [1, 1], [-1, -3], [-3, 2], [4, -1], + [0, 0], [-1, -3], [-3, 2], [4, -1], [-5, -2], [2, 5], [5, 3], [3, -5], [-2, 6], [0, -7], [-4, -6], [-6, 4], [-8, 0], [7, -4], [6, 7], [-7, -8] ], [ - [-4, -7], [-7, -5], [-3, -5], [-5, -4], + [0, 0], [-7, -5], [-3, -5], [-5, -4], [-1, -4], [-2, -2], [-6, -1], [-4, 0], [-7, 1], [-1, 2], [-6, 3], [-3, 3], [-7, 6], [-3, 6], [-5, 7], [-1, 7], diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts index e6904f4c79ad45e2e7688c53131e54d33c6445f2..2b81018aa0fd8d08d647577a64024c705f368ebe 100644 --- a/src/mol-canvas3d/passes/postprocessing.ts +++ b/src/mol-canvas3d/passes/postprocessing.ts @@ -1,11 +1,11 @@ /** - * 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> * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz> */ -import { QuadSchema, QuadValues } from '../../mol-gl/compute/util'; +import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util'; import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema'; import { ShaderCode } from '../../mol-gl/shader-code'; import { WebGLContext } from '../../mol-gl/webgl/context'; @@ -199,6 +199,7 @@ const PostprocessingSchema = { uMaxPossibleViewZDiff: UniformSpec('f'), dOcclusionEnable: DefineSpec('boolean'), + uOcclusionOffset: UniformSpec('v2'), dOutlineEnable: DefineSpec('boolean'), dOutlineScale: DefineSpec('number'), @@ -227,6 +228,7 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d uMaxPossibleViewZDiff: ValueCell.create(0.5), dOcclusionEnable: ValueCell.create(true), + uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)), dOutlineEnable: ValueCell.create(false), dOutlineScale: ValueCell.create(1), @@ -244,9 +246,10 @@ 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 radius is 2^x.' }), + radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }), bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }), blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }), + scaleFactor: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }), }), off: PD.Group({}) }, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }), @@ -281,6 +284,9 @@ export class PostprocessingPass { private readonly ssaoBlurFirstPassFramebuffer: Framebuffer; private readonly ssaoBlurSecondPassFramebuffer: Framebuffer; + private readonly downsampledDepthTarget: RenderTarget; + private readonly downsampleDepthRenderable: CopyRenderable; + private readonly ssaoDepthTexture: Texture; private readonly ssaoDepthBlurProxyTexture: Texture; @@ -290,24 +296,25 @@ export class PostprocessingPass { private nSamples: number; private blurKernelSize: number; + private downsampleFactor: number; private readonly renderable: PostprocessingRenderable; private ssaoScale: number; private calcSsaoScale() { // downscale ssao for high pixel-ratios - return Math.min(1, 1 / this.webgl.pixelRatio); + return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor; } - constructor(private webgl: WebGLContext, drawPass: DrawPass) { - this.ssaoScale = this.calcSsaoScale(); - + constructor(private webgl: WebGLContext, private drawPass: DrawPass) { const { colorTarget, depthTexture } = drawPass; const width = colorTarget.getWidth(); const height = colorTarget.getHeight(); this.nSamples = 1; this.blurKernelSize = 1; + this.downsampleFactor = 1; + this.ssaoScale = this.calcSsaoScale(); // needs to be linear for anti-aliasing pass this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear'); @@ -332,17 +339,20 @@ export class PostprocessingPass { const sw = Math.floor(width * this.ssaoScale); const sh = Math.floor(height * this.ssaoScale); - this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); + this.downsampledDepthTarget = webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear'); + this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTexture); + + this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); this.ssaoDepthTexture.define(sw, sh); this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0'); - this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest'); + this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); this.ssaoDepthBlurProxyTexture.define(sw, sh); this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0'); this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0'); - this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture); + this.ssaoRenderable = getSsaoRenderable(webgl, this.downsampleFactor === 1 ? depthTexture : this.downsampledDepthTarget.texture); this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal'); this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical'); this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture, this.outlinesTarget.texture, this.ssaoDepthTexture); @@ -359,11 +369,13 @@ export class PostprocessingPass { const sh = Math.floor(height * this.ssaoScale); this.target.setSize(width, height); this.outlinesTarget.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.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)); ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh)); @@ -434,6 +446,30 @@ export class PostprocessingPass { ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize); } + if (this.downsampleFactor !== props.occlusion.params.scaleFactor) { + needsUpdateSsao = true; + + this.downsampleFactor = props.occlusion.params.scaleFactor; + this.ssaoScale = this.calcSsaoScale(); + + 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); + + if (this.ssaoScale === 1) { + ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTexture); + } else { + ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture); + } + + 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)); + ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh)); + } } if (props.outline.name === 'on') { @@ -494,6 +530,13 @@ export class PostprocessingPass { gl.scissor(x, y, width, height); } + private occlusionOffset: [x: number, y: number] = [0, 0]; + setOcclusionOffset(x: number, y: number) { + this.occlusionOffset[0] = x; + this.occlusionOffset[1] = y; + ValueCell.update(this.renderable.values.uOcclusionOffset, Vec2.set(this.renderable.values.uOcclusionOffset.ref.value, x, y)); + } + render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) { this.updateState(camera, transparentBackground, backgroundColor, props); @@ -502,14 +545,13 @@ export class PostprocessingPass { this.outlinesRenderable.render(); } - if (props.occlusion.name === 'on') { - const { x, y, width, height } = camera.viewport; - 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); + // 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) { + if (this.ssaoScale < 1) { + this.downsampledDepthTarget.bind(); + this.downsampleDepthRenderable.render(); + } this.ssaoFramebuffer.bind(); this.ssaoRenderable.render(); @@ -519,9 +561,6 @@ export class PostprocessingPass { this.ssaoBlurSecondPassFramebuffer.bind(); this.ssaoBlurSecondPassRenderable.render(); - - this.webgl.gl.viewport(x, y, width, height); - this.webgl.gl.scissor(x, y, width, height); } if (toDrawingBuffer) { diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts index f41c9ac35e3046f565fd4053064b499f78632b41..7cc2f3c7cd1ac9fc4c2860852370adaa9cf469a0 100644 --- a/src/mol-gl/scene.ts +++ b/src/mol-gl/scene.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -79,6 +79,7 @@ interface Scene extends Object3D { has: (o: GraphicsRenderObject) => boolean clear: () => void forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void + getMarkerAverage: () => number } namespace Scene { @@ -243,7 +244,18 @@ namespace Scene { visibleHash = computeVisibleHash(); } return boundingSphereVisible; - } + }, + getMarkerAverage() { + if (primitives.length === 0 && volumes.length === 0) return 0; + let markerAverage = 0; + for (let i = 0, il = primitives.length; i < il; ++i) { + markerAverage += primitives[i].values.markerAverage.ref.value; + } + for (let i = 0, il = volumes.length; i < il; ++i) { + markerAverage += volumes[i].values.markerAverage.ref.value; + } + return markerAverage / (primitives.length + volumes.length); + }, }; } } diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts index 9a50f414352179a0f5e3da916ea961020628f650..aefe55452a3007003fbb9f0ffbdc5998fd854b49 100644 --- a/src/mol-gl/shader/postprocessing.frag.ts +++ b/src/mol-gl/shader/postprocessing.frag.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> * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz> @@ -24,8 +24,7 @@ uniform vec3 uFogColor; uniform vec3 uOutlineColor; uniform bool uTransparentBackground; -uniform float uOcclusionBias; -uniform float uOcclusionRadius; +uniform vec2 uOcclusionOffset; uniform float uMaxPossibleViewZDiff; @@ -102,7 +101,7 @@ void main(void) { if (!isBackground(depth)) { viewDist = abs(getViewZ(depth)); fogFactor = smoothstep(uFogNear, uFogFar, viewDist); - float occlusionFactor = getSsao(coords); + float occlusionFactor = getSsao(coords + uOcclusionOffset); if (!uTransparentBackground) { color.rgb = mix(mix(occlusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor); } else { diff --git a/src/mol-io/common/_spec/encoder.spec.ts b/src/mol-io/common/_spec/encoder.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..10e02317573f4207683a685794fb53a4b61a69c4 --- /dev/null +++ b/src/mol-io/common/_spec/encoder.spec.ts @@ -0,0 +1,28 @@ +import { ArrayEncoding } from '../binary-cif/array-encoder'; +import { decode } from '../binary-cif/decoder'; + +const E = ArrayEncoding; + +test('fixedPoint2', async () => { + const fixedPoint2 = E.by(E.fixedPoint(100)).and(E.delta).and(E.integerPacking); + + const x = [1.092, 1.960, 0.666, 0.480, 1.267]; + const y = [7.428, 7.026, 6.851, 7.524, 8.333]; + const z = [26.270, 26.561, 25.573, 27.055, 25.881]; + + const xEnc = fixedPoint2.encode(new Float32Array(x)); + const yEnc = fixedPoint2.encode(new Float32Array(y)); + const zEnc = fixedPoint2.encode(new Float32Array(z)); + + expect(xEnc.data.length).toEqual(6); + expect(yEnc.data.length).toEqual(5); + expect(zEnc.data.length).toEqual(6); + + const xDec = decode(xEnc); + const yDec = decode(yEnc); + const zDec = decode(zEnc); + + x.forEach((a, i) => expect(xDec[i]).toBeCloseTo(a, 2)); + y.forEach((a, i) => expect(yDec[i]).toBeCloseTo(a, 2)); + z.forEach((a, i) => expect(zDec[i]).toBeCloseTo(a, 2)); +}); diff --git a/src/mol-model-formats/structure/mol2.ts b/src/mol-model-formats/structure/mol2.ts index 40708bef5f43a66a868c8673639c67be3a65d48b..2bdc09eeaf95890f7cda0ca990e952dff94434e6 100644 --- a/src/mol-model-formats/structure/mol2.ts +++ b/src/mol-model-formats/structure/mol2.ts @@ -103,11 +103,11 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) { const flag = Column.ofIntArray(Column.mapToArray(bonds.bond_type, x => { switch (x) { case 'ar': // aromatic + case 'am': // amide return BondType.Flag.Aromatic | BondType.Flag.Covalent; case 'du': // dummy case 'nc': // not connected return BondType.Flag.None; - case 'am': // amide case 'un': // unknown default: return BondType.Flag.Covalent; diff --git a/src/mol-model-formats/structure/property/bonds/chem_comp.ts b/src/mol-model-formats/structure/property/bonds/chem_comp.ts index d53d95f5b637c18d89b5353d0c66ea027a02ca3b..9ca7445f03a5c2399be74343b9071fe80a197aed 100644 --- a/src/mol-model-formats/structure/property/bonds/chem_comp.ts +++ b/src/mol-model-formats/structure/property/bonds/chem_comp.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2017-2020 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2017-2022 Mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -56,10 +56,10 @@ export namespace ComponentBond { const entries: Map<string, Entry> = new Map(); function addEntry(id: string) { - // weird behavior when 'PRO' is requested - will report a single bond between N and H because a later operation would override real content - if (entries.has(id)) { - return entries.get(id)!; - } + // weird behavior when 'PRO' is requested - will report a single bond + // between N and H because a later operation would override real content + if (entries.has(id)) return entries.get(id)!; + const e = new Entry(id); entries.set(id, e); return e; @@ -83,10 +83,8 @@ export namespace ComponentBond { let ord = 1; if (aromatic) flags |= BondType.Flag.Aromatic; switch (order.toLowerCase()) { - case 'doub': - case 'delo': - ord = 2; - break; + case 'delo': flags |= BondType.Flag.Aromatic; break; + case 'doub': ord = 2; break; case 'trip': ord = 3; break; case 'quad': ord = 4; break; } diff --git a/src/mol-model/structure/structure/unit.ts b/src/mol-model/structure/structure/unit.ts index 98bac02731fee07a09159bcadc48e95d103d010b..4c4f0e30975ce1a23fe2c4f97504621d079d814c 100644 --- a/src/mol-model/structure/structure/unit.ts +++ b/src/mol-model/structure/structure/unit.ts @@ -25,6 +25,7 @@ import { Mat4, Vec3 } from '../../../mol-math/linear-algebra'; import { IndexPairBonds } from '../../../mol-model-formats/structure/property/bonds/index-pair'; import { ElementSetIntraBondCache } from './unit/bonds/element-set-intra-bond-cache'; import { ModelSymmetry } from '../../../mol-model-formats/structure/property/symmetry'; +import { getResonance, UnitResonance } from './unit/resonance'; // avoiding namespace lookup improved performance in Chrome (Aug 2020) const v3add = Vec3.add; @@ -308,6 +309,12 @@ namespace Unit { return this.props.rings; } + get resonance() { + if (this.props.resonance) return this.props.resonance; + this.props.resonance = getResonance(this); + return this.props.resonance; + } + get polymerElements() { if (this.props.polymerElements) return this.props.polymerElements; this.props.polymerElements = getAtomicPolymerElements(this); @@ -368,6 +375,7 @@ namespace Unit { interface AtomicProperties extends BaseProperties { bonds?: IntraUnitBonds rings?: UnitRings + resonance?: UnitResonance nucleotideElements?: SortedArray<ElementIndex> proteinElements?: SortedArray<ElementIndex> residueCount?: number diff --git a/src/mol-model/structure/structure/unit/resonance.ts b/src/mol-model/structure/structure/unit/resonance.ts new file mode 100644 index 0000000000000000000000000000000000000000..f7f17c3cccb25df2cc152a8a7d1524ba3cd027fd --- /dev/null +++ b/src/mol-model/structure/structure/unit/resonance.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { SortedArray } from '../../../../mol-data/int/sorted-array'; +import { sortedCantorPairing } from '../../../../mol-data/util'; +import { BondType } from '../../model/types'; +import { StructureElement } from '../element'; +import { Unit } from '../unit'; + +export type UnitResonance = { + /** + * Lookup for triplets of atoms in delocalized bonds. + * + * Does not include triplets that are part of aromatic rings. + */ + readonly delocalizedTriplets: { + /** Return 3rd element in triplet or undefined if `a` and `b` are not part of a triplet */ + readonly getThirdElement: (a: StructureElement.UnitIndex, b: StructureElement.UnitIndex) => StructureElement.UnitIndex | undefined + /** Return index into `triplets` or undefined if `a` is not part of any triplet */ + readonly getTripletIndices: (a: StructureElement.UnitIndex) => number[] | undefined + readonly triplets: SortedArray<StructureElement.UnitIndex>[] + } +} + +export function getResonance(unit: Unit.Atomic): UnitResonance { + return { + delocalizedTriplets: getDelocalizedTriplets(unit) + }; +} + +function getDelocalizedTriplets(unit: Unit.Atomic) { + const bonds = unit.bonds; + const { b, edgeProps, offset } = bonds; + const { order: _order, flags: _flags } = edgeProps; + const { elementAromaticRingIndices } = unit.rings; + + const triplets: SortedArray<StructureElement.UnitIndex>[] = []; + const thirdElementMap = new Map<number, StructureElement.UnitIndex>(); + const indicesMap = new Map<number, number[]>(); + + const add = (a: StructureElement.UnitIndex, b: StructureElement.UnitIndex, c: StructureElement.UnitIndex) => { + const index = triplets.length; + triplets.push(SortedArray.ofUnsortedArray([a, b, c])); + thirdElementMap.set(sortedCantorPairing(a, b), c); + if (indicesMap.has(a)) indicesMap.get(a)!.push(index); + else indicesMap.set(a, [index]); + }; + + for (let i = 0 as StructureElement.UnitIndex; i < unit.elements.length; i++) { + if (elementAromaticRingIndices.has(i)) continue; + + const count = offset[i + 1] - offset[i] + 1; + if (count < 2) continue; + + const deloBonds: StructureElement.UnitIndex[] = []; + for (let t = offset[i], _t = offset[i + 1]; t < _t; t++) { + const f = _flags[t]; + if (!BondType.is(f, BondType.Flag.Aromatic)) continue; + + deloBonds.push(b[t]); + } + + if (deloBonds.length >= 2) { + add(i, deloBonds[0], deloBonds[1]); + for (let j = 1, jl = deloBonds.length; j < jl; j++) { + add(i, deloBonds[j], deloBonds[0]); + } + } + } + + return { + getThirdElement: (a: StructureElement.UnitIndex, b: StructureElement.UnitIndex) => { + return thirdElementMap.get(sortedCantorPairing(a, b)); + }, + getTripletIndices: (a: StructureElement.UnitIndex) => { + return indicesMap.get(a); + }, + triplets, + }; +} diff --git a/src/mol-model/structure/structure/unit/rings/compute.ts b/src/mol-model/structure/structure/unit/rings/compute.ts index 672521561297c43afedd74b21f188922158365bd..074a5f9742fa6f53342ec23af23aab87c976ce36 100644 --- a/src/mol-model/structure/structure/unit/rings/compute.ts +++ b/src/mol-model/structure/structure/unit/rings/compute.ts @@ -28,17 +28,19 @@ export function computeRings(unit: Unit.Atomic) { } const enum Constants { - MaxDepth = 4 + MaxDepth = 5 } interface State { startVertex: number, endVertex: number, count: number, - visited: Int32Array, + isRingAtom: Int32Array, + marked: Int32Array, queue: Int32Array, color: Int32Array, pred: Int32Array, + depth: Int32Array, left: Int32Array, right: Int32Array, @@ -59,9 +61,11 @@ function State(unit: Unit.Atomic, capacity: number): State { startVertex: 0, endVertex: 0, count: 0, - visited: new Int32Array(capacity), + isRingAtom: new Int32Array(capacity), + marked: new Int32Array(capacity), queue: new Int32Array(capacity), pred: new Int32Array(capacity), + depth: new Int32Array(capacity), left: new Int32Array(Constants.MaxDepth), right: new Int32Array(Constants.MaxDepth), color: new Int32Array(capacity), @@ -78,17 +82,26 @@ function State(unit: Unit.Atomic, capacity: number): State { function resetState(state: State) { state.count = state.endVertex - state.startVertex; - const { visited, pred, color } = state; + const { isRingAtom, pred, color, depth, marked } = state; for (let i = 0; i < state.count; i++) { - visited[i] = -1; + isRingAtom[i] = 0; pred[i] = -1; + marked[i] = -1; color[i] = 0; + depth[i] = 0; } state.currentColor = 0; state.currentAltLoc = ''; state.hasAltLoc = false; } +function resetDepth(state: State) { + const { depth } = state; + for (let i = 0; i < state.count; i++) { + depth[i] = state.count + 1; + } +} + function largestResidue(unit: Unit.Atomic) { const residuesIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements); let size = 0; @@ -99,8 +112,16 @@ function largestResidue(unit: Unit.Atomic) { return size; } +function isStartIndex(state: State, i: number) { + const bondOffset = state.bonds.offset; + const a = state.startVertex + i; + const bStart = bondOffset[a], bEnd = bondOffset[a + 1]; + const bondCount = bEnd - bStart; + if (bondCount <= 1 || (state.isRingAtom[i] && bondCount === 2)) return false; + return true; +} + function processResidue(state: State, start: number, end: number) { - const { visited } = state; state.startVertex = start; state.endVertex = end; @@ -117,11 +138,13 @@ function processResidue(state: State, start: number, end: number) { } arraySetRemove(altLocs, ''); + let mark = 1; if (altLocs.length === 0) { resetState(state); for (let i = 0; i < state.count; i++) { - if (visited[i] >= 0) continue; - findRings(state, i); + if (!isStartIndex(state, i)) continue; + resetDepth(state); + mark = findRings(state, i, mark); } } else { for (let aI = 0; aI < altLocs.length; aI++) { @@ -129,12 +152,13 @@ function processResidue(state: State, start: number, end: number) { state.hasAltLoc = true; state.currentAltLoc = altLocs[aI]; for (let i = 0; i < state.count; i++) { - if (visited[i] >= 0) continue; + if (!isStartIndex(state, i)) continue; const altLoc = state.altLoc.value(elements[state.startVertex + i]); if (altLoc && altLoc !== state.currentAltLoc) { continue; } - findRings(state, i); + resetDepth(state); + mark = findRings(state, i, mark); } } } @@ -144,10 +168,10 @@ function processResidue(state: State, start: number, end: number) { } } -function addRing(state: State, a: number, b: number) { +function addRing(state: State, a: number, b: number, isRingAtom: Int32Array) { // only "monotonous" rings if (b < a) { - return; + return false; } const { pred, color, left, right } = state; @@ -176,7 +200,7 @@ function addRing(state: State, a: number, b: number) { if (current < 0) break; } if (!found) { - return; + return false; } current = a; @@ -190,50 +214,50 @@ function addRing(state: State, a: number, b: number) { const len = leftOffset + rightOffset; // rings must have at least three elements if (len < 3) { - return; + return false; } const ring = new Int32Array(len); let ringOffset = 0; - for (let t = 0; t < leftOffset; t++) ring[ringOffset++] = state.startVertex + left[t]; - for (let t = rightOffset - 1; t >= 0; t--) ring[ringOffset++] = state.startVertex + right[t]; + for (let t = 0; t < leftOffset; t++) { + ring[ringOffset++] = state.startVertex + left[t]; + isRingAtom[left[t]] = 1; + } + for (let t = rightOffset - 1; t >= 0; t--) { + ring[ringOffset++] = state.startVertex + right[t]; + isRingAtom[right[t]] = 1; + } sortArray(ring); - if (state.hasAltLoc) { - // we need to check if the ring was already added because alt locs are present. - - for (let rI = 0, _rI = state.currentRings.length; rI < _rI; rI++) { - const r = state.currentRings[rI]; - if (ring[0] !== r[0]) continue; - if (ring.length !== r.length) continue; + // Check if the ring is unique and another one is not it's subset + for (let rI = 0, _rI = state.currentRings.length; rI < _rI; rI++) { + const r = state.currentRings[rI]; - let areSame = true; - for (let aI = 0, _aI = ring.length; aI < _aI; aI++) { - if (ring[aI] !== r[aI]) { - areSame = false; - break; - } - } - if (areSame) { - return; - } + if (ring.length === r.length) { + if (SortedArray.areEqual(ring as any, r)) return false; + } else if (ring.length > r.length) { + if (SortedArray.isSubset(ring as any, r)) return false; } } state.currentRings.push(SortedArray.ofSortedArray(ring)); + + return true; } -function findRings(state: State, from: number) { - const { bonds, startVertex, endVertex, visited, queue, pred } = state; +function findRings(state: State, from: number, mark: number) { + const { bonds, startVertex, endVertex, isRingAtom, marked, queue, pred, depth } = state; const { elements } = state.unit; const { b: neighbor, edgeProps: { flags: bondFlags }, offset } = bonds; - visited[from] = 1; + marked[from] = mark; + depth[from] = 0; queue[0] = from; let head = 0, size = 1; while (head < size) { const top = queue[head++]; + const d = depth[top]; const a = startVertex + top; const start = offset[a], end = offset[a + 1]; @@ -250,18 +274,25 @@ function findRings(state: State, from: number) { const other = b - startVertex; - if (visited[other] > 0) { + if (marked[other] === mark) { if (pred[other] !== top && pred[top] !== other) { - addRing(state, top, other); + if (addRing(state, top, other, isRingAtom)) { + return mark + 1; + } } continue; } - visited[other] = 1; + const newDepth = Math.min(depth[other], d + 1); + if (newDepth > Constants.MaxDepth) continue; + + depth[other] = newDepth; + marked[other] = mark; queue[size++] = other; pred[other] = top; } } + return mark + 1; } export function getFingerprint(elements: string[]) { diff --git a/src/mol-plugin-ui/structure/quick-styles.tsx b/src/mol-plugin-ui/structure/quick-styles.tsx index bc5cf66123d7db4a82441d7c129af33ebde8b417..1449eea3e44c7cabb63573643aa8bcbbb2d1eba0 100644 --- a/src/mol-plugin-ui/structure/quick-styles.tsx +++ b/src/mol-plugin-ui/structure/quick-styles.tsx @@ -60,7 +60,7 @@ export class QuickStyles extends PurePluginUIComponent { }, occlusion: { name: 'on', - params: { bias: 0.9, blurKernelSize: 15, radius: 5, samples: 32 } + params: { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, scaleFactor: 1 } }, } }); @@ -84,7 +84,7 @@ export class QuickStyles extends PurePluginUIComponent { name: 'on', params: pp.occlusion.name === 'on' ? pp.occlusion.params - : { bias: 0.9, blurKernelSize: 15, radius: 5, samples: 32 } + : { bias: 0.8, blurKernelSize: 15, radius: 5, samples: 32, scaleFactor: 1 } }, } }); diff --git a/src/mol-plugin/util/viewport-screenshot.ts b/src/mol-plugin/util/viewport-screenshot.ts index fc95938fae5b9e77fa459c6d9982dcd2b90ca851..722c605fdf5cd2fb93fe790fefe0e2d2721b887c 100644 --- a/src/mol-plugin/util/viewport-screenshot.ts +++ b/src/mol-plugin/util/viewport-screenshot.ts @@ -119,7 +119,7 @@ class ViewportScreenshotHelper extends PluginComponent { postprocessing: { ...c.props.postprocessing, occlusion: aoProps.name === 'on' - ? { name: 'on', params: { ...aoProps.params, samples: 128 } } + ? { name: 'on', params: { ...aoProps.params, samples: 128, scaleFactor: 1 } } : aoProps }, marking: { ...c.props.marking } @@ -143,7 +143,7 @@ class ViewportScreenshotHelper extends PluginComponent { postprocessing: { ...c.props.postprocessing, occlusion: aoProps.name === 'on' - ? { name: 'on', params: { ...aoProps.params, samples: 128 } } + ? { name: 'on', params: { ...aoProps.params, samples: 128, scaleFactor: 1 } } : aoProps }, marking: { ...c.props.marking } diff --git a/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts b/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts index 6a7db30142e4c8b2954d145df5e0a7b56f55473d..58f762b2a1f405dcf8d5f2b7cf6ff8791dbe9626 100644 --- a/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts +++ b/src/mol-repr/structure/visual/bond-intra-unit-cylinder.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> * @author David Sehnal <david.sehnal@gmail.com> @@ -79,12 +79,16 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru }; const { elementRingIndices, elementAromaticRingIndices } = unit.rings; + const deloTriplets = aromaticBonds ? unit.resonance.delocalizedTriplets : undefined; return { linkCount: edgeCount * 2, referencePosition: (edgeIndex: number) => { let aI = a[edgeIndex], bI = b[edgeIndex]; + const rI = deloTriplets?.getThirdElement(aI, bI); + if (rI !== undefined) return pos(elements[rI], vRef); + if (aI > bI) [aI, bI] = [bI, aI]; if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI]; @@ -145,8 +149,10 @@ function getIntraUnitBondCylinderBuilderProps(unit: Unit.Atomic, structure: Stru if (isBondType(f, BondType.Flag.Aromatic) || (arCount && !ignoreComputedAromatic)) { if (arCount === 2) { return LinkStyle.MirroredAromatic; - } else { + } else if (arCount === 1 || deloTriplets?.getThirdElement(aI, bI)) { return LinkStyle.Aromatic; + } else { + // case for bonds between two aromatic rings } } } diff --git a/src/mol-repr/structure/visual/bond-intra-unit-line.ts b/src/mol-repr/structure/visual/bond-intra-unit-line.ts index df34bfba5e1600715a360ddc9e96eee74e8ca163..7abcf9376d73c82a5f0b6a24a10509b3f4eddbae 100644 --- a/src/mol-repr/structure/visual/bond-intra-unit-line.ts +++ b/src/mol-repr/structure/visual/bond-intra-unit-line.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info. + * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author Alexander Rose <alexander.rose@weirdbyte.de> */ @@ -52,12 +52,16 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str const pos = unit.conformation.invariantPosition; const { elementRingIndices, elementAromaticRingIndices } = unit.rings; + const deloTriplets = aromaticBonds ? unit.resonance.delocalizedTriplets : undefined; const builderProps: LinkBuilderProps = { linkCount: edgeCount * 2, referencePosition: (edgeIndex: number) => { let aI = a[edgeIndex], bI = b[edgeIndex]; + const rI = deloTriplets?.getThirdElement(aI, bI); + if (rI !== undefined) return pos(elements[rI], vRef); + if (aI > bI) [aI, bI] = [bI, aI]; if (offset[aI + 1] - offset[aI] === 1) [aI, bI] = [bI, aI]; @@ -106,8 +110,10 @@ function createIntraUnitBondLines(ctx: VisualContext, unit: Unit, structure: Str if (isBondType(f, BondType.Flag.Aromatic) || (arCount && !ignoreComputedAromatic)) { if (arCount === 2) { return LinkStyle.MirroredAromatic; - } else { + } else if (arCount === 1 || deloTriplets?.getThirdElement(aI, bI)) { return LinkStyle.Aromatic; + } else { + // case for bonds between two aromatic rings } } } diff --git a/src/mol-repr/structure/visual/util/bond.ts b/src/mol-repr/structure/visual/util/bond.ts index e3f44534b33259eecd66ace91d506423c68af4e9..61086d2585c30e9da0ba80ce8ec7338581bdac45 100644 --- a/src/mol-repr/structure/visual/util/bond.ts +++ b/src/mol-repr/structure/visual/util/bond.ts @@ -264,4 +264,4 @@ export function eachInterBond(loci: Loci, structure: Structure, apply: (interval __unitMap.clear(); } return changed; -} \ No newline at end of file +}