diff --git a/CHANGELOG.md b/CHANGELOG.md index 436011206a86d01010049a0fbd256f15457ab95a..baead67420e40ca97abb5c7da5c30bef0c940ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Note that since we don't clearly distinguish between a public and private interf ## [Unreleased] +- Expose inter-bonds compute params in structure +- Improve performance of inter/intra-bonds compute - Fix defaultAttribs handling in Canvas3DContext.fromCanvas - Confal pyramids extension improvements - Add custom labels to Confal pyramids @@ -13,8 +15,26 @@ Note that since we don't clearly distinguish between a public and private interf - Add example mmCIF file with categories necessary to display Confal pyramids - Change the lookup logic of NtC steps from residues - Add support for download of gzipped files +- Don't filter IndexPairBonds by element-based rules in MOL/SDF and MOL2 (without symmetry) models - Fix Glycam Saccharide Names used by default - Fix GPU surfaces rendering in Safari with WebGL2 +- Add ``fov`` (Field of View) Canvas3D parameter +- Add ``sceneRadiusFactor`` Canvas3D parameter +- Add background pass (skybox, image, horizontal/radial gradient) + - Set simple-settings presets via ``PluginConfig.Background.Styles`` + - Example presets in new backgrounds extension + - Load skybox/image from URL or File (saved in session) + - Opacity, saturation, lightness controls for skybox/image + - Coverage (viewport or canvas) controls for image/gradient +- [Breaking] ``AssetManager`` needs to be passed to various graphics related classes +- Fix SSAO renderable initialization +- Reduce number of webgl state changes + - Add ``viewport`` and ``scissor`` to state object + - Add ``hasOpaque`` to scene object +- Handle edge cases where some renderables would not get (correctly) rendered + - Fix text background rendering for opaque text + - Fix helper scenes not shown when rendering directly to draw target +- Fix ``CustomElementProperty`` coloring not working ## [v3.13.0] - 2022-07-24 diff --git a/package.json b/package.json index d427684401256baefbdd864fa63adcbc400aecca..7e827994e88b9caab3066949355685d559f866b9 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "rebuild": "npm run clean && npm run build", "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer", "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"", - "build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/", + "build-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/", "build-webpack": "webpack --mode production --config ./webpack.config.production.js", "build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js", "watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"", @@ -28,7 +28,7 @@ "watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"", "watch-tsc": "tsc --watch --incremental", "watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental", - "watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch", + "watch-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/ --watch", "watch-webpack": "webpack -w --mode development --stats minimal", "watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js", "watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js", diff --git a/src/apps/viewer/app.ts b/src/apps/viewer/app.ts index 2ab5c5eafb53157ff9ca975cb4041cbf990f8167..8a27a9a4cd9762fc7686d621b145ddc0faed6bfa 100644 --- a/src/apps/viewer/app.ts +++ b/src/apps/viewer/app.ts @@ -46,6 +46,7 @@ import { Color } from '../../mol-util/color'; import '../../mol-util/polyfill'; import { ObjectKeys } from '../../mol-util/type-helpers'; import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants'; +import { Backgrounds } from '../../extensions/backgrounds'; export { PLUGIN_VERSION as version } from '../../mol-plugin/version'; export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug'; @@ -55,6 +56,7 @@ const CustomFormats = [ ]; const Extensions = { + 'backgrounds': PluginSpec.Behavior(Backgrounds), 'cellpack': PluginSpec.Behavior(CellPack), 'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids), 'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport), diff --git a/src/extensions/backgrounds/images/cells.jpg b/src/extensions/backgrounds/images/cells.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3502c798c5d012acd67640ca8cbbb2341014c71f Binary files /dev/null and b/src/extensions/backgrounds/images/cells.jpg differ diff --git a/src/extensions/backgrounds/index.ts b/src/extensions/backgrounds/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c87c8899f367a38bf377f6d266586faf0fe32ae0 --- /dev/null +++ b/src/extensions/backgrounds/index.ts @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { PluginBehavior } from '../../mol-plugin/behavior/behavior'; +import { PluginConfig } from '../../mol-plugin/config'; +import { Color } from '../../mol-util/color/color'; + +// from https://visualsonline.cancer.gov/details.cfm?imageid=2304, public domain +import image_cells from './images/cells.jpg'; + +// created with http://alexcpeterson.com/spacescape/ +import face_nebula_nx from './skyboxes/nebula/nebula_left2.jpg'; +import face_nebula_ny from './skyboxes/nebula/nebula_bottom4.jpg'; +import face_nebula_nz from './skyboxes/nebula/nebula_back6.jpg'; +import face_nebula_px from './skyboxes/nebula/nebula_right1.jpg'; +import face_nebula_py from './skyboxes/nebula/nebula_top3.jpg'; +import face_nebula_pz from './skyboxes/nebula/nebula_front5.jpg'; + +export const Backgrounds = PluginBehavior.create<{ }>({ + name: 'extension-backgrounds', + category: 'misc', + display: { + name: 'Backgrounds' + }, + ctor: class extends PluginBehavior.Handler<{ }> { + register(): void { + this.ctx.config.set(PluginConfig.Background.Styles, [ + [{ + variant: { + name: 'radialGradient', + params: { + centerColor: Color(0xFFFFFF), + edgeColor: Color(0x808080), + ratio: 0.2, + coverage: 'viewport', + } + } + }, 'Light Radial Gradient'], + [{ + variant: { + name: 'image', + params: { + source: { + name: 'url', + params: image_cells + }, + lightness: 0, + saturation: 0, + opacity: 1, + coverage: 'viewport', + } + } + }, 'Normal Cells Image'], + [{ + variant: { + name: 'skybox', + params: { + faces: { + name: 'urls', + params: { + nx: face_nebula_nx, + ny: face_nebula_ny, + nz: face_nebula_nz, + px: face_nebula_px, + py: face_nebula_py, + pz: face_nebula_pz, + } + }, + lightness: 0, + saturation: 0, + opacity: 1, + } + } + }, 'Purple Nebula Skybox'], + ]); + } + + update() { + return false; + } + + unregister() { + this.ctx.config.set(PluginConfig.Background.Styles, []); + } + }, + params: () => ({ }) +}); diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4e2f0fd8d977272a4bc2af9cf982fe866eb52186 Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2be6e805e2ea0103230cf51637218e115cf0a76d Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e9c0674db6f28f3d0fa02421e473517790d51f2d Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..810037b66dc4601b24b1b1b9adf514f626247b6c Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..059d46bf8e7d8d18ce12259679f0c53ad4a5dc27 Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg differ diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..831b81964a253fbbd08b0ba3aad74100ce55fad1 Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg differ diff --git a/src/extensions/backgrounds/typings.d.ts b/src/extensions/backgrounds/typings.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..83e4393576a0b35df23ec0583a74e20387a0b97e --- /dev/null +++ b/src/extensions/backgrounds/typings.d.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +declare module '*.jpg' { + const value: string; + export = value; +} diff --git a/src/extensions/mp4-export/encoder.ts b/src/extensions/mp4-export/encoder.ts index 5dc12af421808abee78e69e341f7b4d7ae263e4f..38e96599220a377c94704939d9a951be46d1ff17 100644 --- a/src/extensions/mp4-export/encoder.ts +++ b/src/extensions/mp4-export/encoder.ts @@ -69,6 +69,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: const dt = durationMs / N; await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 }); + await params.pass.updateBackground(); await plugin.managers.animation.play(params.animation.definition, params.animation.params); stoppedAnimation = false; diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index edb4d15f0dd17a5696439ef2421ab9a587d08407..5df5536af201a1b921c1a5c84a9dba55642f9afe 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -40,6 +40,8 @@ import { Passes } from './passes/passes'; import { shallowEqual } from '../mol-util'; import { MarkingParams } from './passes/marking'; import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit } from '../mol-gl/webgl/render-item'; +import { degToRad, radToDeg } from '../mol-math/misc'; +import { AssetManager } from '../mol-util/assets'; export const Canvas3DParams = { camera: PD.Group({ @@ -49,6 +51,7 @@ export const Canvas3DParams = { on: PD.Group(StereoCameraParams), off: PD.Group({}) }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }), + fov: PD.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }), manualReset: PD.Boolean(false, { isHidden: true }), }, { pivot: 'mode' }), cameraFog: PD.MappedStatic('on', { @@ -78,6 +81,7 @@ export const Canvas3DParams = { }), cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }), + sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }), transparentBackground: PD.Boolean(false), multiSample: PD.Group(MultiSampleParams), @@ -106,6 +110,7 @@ interface Canvas3DContext { readonly attribs: Readonly<Canvas3DContext.Attribs> readonly contextLost: BehaviorSubject<now.Timestamp> readonly contextRestored: BehaviorSubject<now.Timestamp> + readonly assetManager: AssetManager dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void } @@ -124,7 +129,7 @@ namespace Canvas3DContext { }; export type Attribs = typeof DefaultAttribs - export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext { + export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext { const a = { ...DefaultAttribs, ...attribs }; const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a; const gl = getGLContext(canvas, { @@ -139,7 +144,7 @@ namespace Canvas3DContext { const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true }); const webgl = createContext(gl, { pixelScale }); - const passes = new Passes(webgl, a); + const passes = new Passes(webgl, assetManager, a); if (isDebugMode) { const loseContextExt = gl.getExtension('WEBGL_lose_context'); @@ -192,6 +197,7 @@ namespace Canvas3DContext { attribs: a, contextLost, contextRestored: webgl.contextRestored, + assetManager, dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => { input.dispose(); @@ -278,7 +284,7 @@ namespace Canvas3D { export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 } export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 } - export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D { + export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D { const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props }; const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>(); @@ -299,11 +305,16 @@ namespace Canvas3D { const scene = Scene.create(webgl, passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended); + function getSceneRadius() { + return scene.boundingSphere.radius * p.sceneRadiusFactor; + } + const camera = new Camera({ position: Vec3.create(0, 0, 100), mode: p.camera.mode, fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0, - clipFar: p.cameraClipping.far + clipFar: p.cameraClipping.far, + fov: degToRad(p.camera.fov), }, { x, y, width, height }, { pixelScale: attribs.pixelScale }); const stereoCamera = new StereoCamera(camera, p.camera.stereo.params); @@ -315,6 +326,10 @@ namespace Canvas3D { const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction); const multiSampleHelper = new MultiSampleHelper(passes.multiSample); + passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => { + if (changed) requestDraw(); + }); + let cameraResetRequested = false; let nextCameraResetDuration: number | undefined = void 0; let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0; @@ -523,7 +538,7 @@ namespace Canvas3D { const focus = camera.getFocus(center, radius); const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot; const snapshot = next ? { ...focus, ...next } : focus; - camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration); + camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration); } nextCameraResetDuration = void 0; @@ -574,7 +589,7 @@ namespace Canvas3D { } if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0; - if (!p.camera.manualReset) camera.setState({ radiusMax: scene.boundingSphere.radius }, 0); + if (!p.camera.manualReset) camera.setState({ radiusMax: getSceneRadius() }, 0); reprCount.next(reprRenderObjects.size); if (isDebugMode) consoleStats(); @@ -650,7 +665,7 @@ namespace Canvas3D { function getProps(): Canvas3DProps { const radius = scene.boundingSphere.radius > 0 - ? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100) + ? 100 - Math.round((camera.transition.target.radius / getSceneRadius()) * 100) : 0; return { @@ -658,6 +673,7 @@ namespace Canvas3D { mode: camera.state.mode, helper: { ...helper.camera.props }, stereo: { ...p.camera.stereo }, + fov: Math.round(radToDeg(camera.state.fov)), manualReset: !!p.camera.manualReset }, cameraFog: camera.state.fog > 0 @@ -665,6 +681,7 @@ namespace Canvas3D { : { name: 'off' as const, params: {} }, cameraClipping: { far: camera.state.clipFar, radius }, cameraResetDurationMs: p.cameraResetDurationMs, + sceneRadiusFactor: p.sceneRadiusFactor, transparentBackground: p.transparentBackground, viewport: p.viewport, @@ -767,10 +784,19 @@ namespace Canvas3D { ? produce(getProps(), properties as any) : properties; + if (props.sceneRadiusFactor !== undefined) { + p.sceneRadiusFactor = props.sceneRadiusFactor; + camera.setState({ radiusMax: getSceneRadius() }, 0); + } + const cameraState: Partial<Camera.Snapshot> = Object.create(null); if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) { cameraState.mode = props.camera.mode; } + const oldFov = Math.round(radToDeg(camera.state.fov)); + if (props.camera && props.camera.fov !== undefined && props.camera.fov !== oldFov) { + cameraState.fov = degToRad(props.camera.fov); + } if (props.cameraFog !== undefined && props.cameraFog.params) { const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0; if (newFog !== camera.state.fog) cameraState.fog = newFog; @@ -780,7 +806,7 @@ namespace Canvas3D { cameraState.clipFar = props.cameraClipping.far; } if (props.cameraClipping.radius !== undefined) { - const radius = (scene.boundingSphere.radius / 100) * (100 - props.cameraClipping.radius); + const radius = (getSceneRadius() / 100) * (100 - props.cameraClipping.radius); if (radius > 0 && radius !== cameraState.radius) { // if radius = 0, NaNs happen cameraState.radius = Math.max(radius, 0.01); @@ -805,6 +831,12 @@ namespace Canvas3D { } } + if (props.postprocessing?.background) { + Object.assign(p.postprocessing.background, props.postprocessing.background); + passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => { + if (changed && !doNotRequestDraw) requestDraw(); + }); + } if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing); if (props.marking) Object.assign(p.marking, props.marking); if (props.multiSample) Object.assign(p.multiSample, props.multiSample); @@ -823,7 +855,7 @@ namespace Canvas3D { } }, getImagePass: (props: Partial<ImageProps> = {}) => { - return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props); + return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, passes.draw.wboitEnabled, props); }, getRenderObjects(): GraphicsRenderObject[] { const renderObjects: GraphicsRenderObject[] = []; diff --git a/src/mol-canvas3d/passes/background.ts b/src/mol-canvas3d/passes/background.ts new file mode 100644 index 0000000000000000000000000000000000000000..d4bfb3e59aa3fb02cfdc4e958b61618dbc500235 --- /dev/null +++ b/src/mol-canvas3d/passes/background.ts @@ -0,0 +1,461 @@ +/** + * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { QuadPositions, } from '../../mol-gl/compute/util'; +import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable'; +import { AttributeSpec, DefineSpec, TextureSpec, UniformSpec, Values, ValueSpec } from '../../mol-gl/renderable/schema'; +import { ShaderCode } from '../../mol-gl/shader-code'; +import { background_frag } from '../../mol-gl/shader/background.frag'; +import { background_vert } from '../../mol-gl/shader/background.vert'; +import { WebGLContext } from '../../mol-gl/webgl/context'; +import { createComputeRenderItem } from '../../mol-gl/webgl/render-item'; +import { createNullTexture, CubeFaces, Texture } from '../../mol-gl/webgl/texture'; +import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4'; +import { ValueCell } from '../../mol-util/value-cell'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { isTimingMode } from '../../mol-util/debug'; +import { Camera, ICamera } from '../camera'; +import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3'; +import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2'; +import { Color } from '../../mol-util/color'; +import { Asset, AssetManager } from '../../mol-util/assets'; +import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4'; + +const SharedParams = { + opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }), + saturation: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }), + lightness: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }), +}; + +const SkyboxParams = { + faces: PD.MappedStatic('urls', { + urls: PD.Group({ + nx: PD.Text('', { label: 'Negative X / Left' }), + ny: PD.Text('', { label: 'Negative Y / Bottom' }), + nz: PD.Text('', { label: 'Negative Z / Back' }), + px: PD.Text('', { label: 'Positive X / Right' }), + py: PD.Text('', { label: 'Positive Y / Top' }), + pz: PD.Text('', { label: 'Positive Z / Front' }), + }, { isExpanded: true, label: 'URLs' }), + files: PD.Group({ + nx: PD.File({ label: 'Negative X / Left', accept: 'image/*' }), + ny: PD.File({ label: 'Negative Y / Bottom', accept: 'image/*' }), + nz: PD.File({ label: 'Negative Z / Back', accept: 'image/*' }), + px: PD.File({ label: 'Positive X / Right', accept: 'image/*' }), + py: PD.File({ label: 'Positive Y / Top', accept: 'image/*' }), + pz: PD.File({ label: 'Positive Z / Front', accept: 'image/*' }), + }, { isExpanded: true, label: 'Files' }), + }), + ...SharedParams, +}; +type SkyboxProps = PD.Values<typeof SkyboxParams> + +const ImageParams = { + source: PD.MappedStatic('url', { + url: PD.Text(''), + file: PD.File({ accept: 'image/*' }), + }), + ...SharedParams, + coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])), +}; +type ImageProps = PD.Values<typeof ImageParams> + +const HorizontalGradientParams = { + topColor: PD.Color(Color(0xDDDDDD)), + bottomColor: PD.Color(Color(0xEEEEEE)), + ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), + coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])), +}; + +const RadialGradientParams = { + centerColor: PD.Color(Color(0xDDDDDD)), + edgeColor: PD.Color(Color(0xEEEEEE)), + ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }), + coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])), +}; + +export const BackgroundParams = { + variant: PD.MappedStatic('off', { + off: PD.EmptyGroup(), + skybox: PD.Group(SkyboxParams, { isExpanded: true }), + image: PD.Group(ImageParams, { isExpanded: true }), + horizontalGradient: PD.Group(HorizontalGradientParams, { isExpanded: true }), + radialGradient: PD.Group(RadialGradientParams, { isExpanded: true }), + }, { label: 'Environment' }), +}; +export type BackgroundProps = PD.Values<typeof BackgroundParams> + +export class BackgroundPass { + private renderable: BackgroundRenderable; + + private skybox: { + texture: Texture + props: SkyboxProps + assets: Asset[] + loaded: boolean + } | undefined; + + private image: { + texture: Texture + props: ImageProps + asset: Asset + loaded: boolean + } | undefined; + + private readonly camera = new Camera(); + private readonly target = Vec3(); + private readonly position = Vec3(); + private readonly dir = Vec3(); + + readonly texture: Texture; + + constructor(private readonly webgl: WebGLContext, private readonly assetManager: AssetManager, width: number, height: number) { + this.renderable = getBackgroundRenderable(webgl, width, height); + } + + setSize(width: number, height: number) { + const [w, h] = this.renderable.values.uTexSize.ref.value; + + if (width !== w || height !== h) { + ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height)); + } + } + + private clearSkybox() { + if (this.skybox !== undefined) { + this.skybox.texture.destroy(); + this.skybox.assets.forEach(a => this.assetManager.release(a)); + this.skybox = undefined; + } + } + + private updateSkybox(camera: ICamera, props: SkyboxProps, onload?: (changed: boolean) => void) { + const tf = this.skybox?.props.faces; + const f = props.faces.params; + if (!f.nx || !f.ny || !f.nz || !f.px || !f.py || !f.pz) { + this.clearSkybox(); + onload?.(false); + return; + } + if (!this.skybox || !tf || !areSkyboxTexturePropsEqual(props.faces, this.skybox.props.faces)) { + this.clearSkybox(); + const { texture, assets } = getSkyboxTexture(this.webgl, this.assetManager, props.faces, errored => { + if (this.skybox) this.skybox.loaded = !errored; + onload?.(true); + }); + this.skybox = { texture, props: { ...props }, assets, loaded: false }; + ValueCell.update(this.renderable.values.tSkybox, texture); + this.renderable.update(); + } else { + onload?.(false); + } + if (!this.skybox) return; + + let cam = camera; + if (camera.state.mode === 'orthographic') { + this.camera.setState({ ...camera.state, mode: 'perspective' }); + this.camera.update(); + cam = this.camera; + } + + const m = this.renderable.values.uViewDirectionProjectionInverse.ref.value; + Vec3.sub(this.dir, cam.state.position, cam.state.target); + Vec3.setMagnitude(this.dir, this.dir, 0.1); + Vec3.copy(this.position, this.dir); + Mat4.lookAt(m, this.position, this.target, cam.state.up); + Mat4.mul(m, cam.projection, m); + Mat4.invert(m, m); + ValueCell.update(this.renderable.values.uViewDirectionProjectionInverse, m); + + ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity); + ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation); + ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness); + ValueCell.updateIfChanged(this.renderable.values.dVariant, 'skybox'); + this.renderable.update(); + } + + private clearImage() { + if (this.image !== undefined) { + this.image.texture.destroy(); + this.assetManager.release(this.image.asset); + this.image = undefined; + } + } + + private updateImage(props: ImageProps, onload?: (loaded: boolean) => void) { + if (!props.source.params) { + this.clearImage(); + onload?.(false); + return; + } + if (!this.image || !this.image.props.source.params || !areImageTexturePropsEqual(props.source, this.image.props.source)) { + this.clearImage(); + const { texture, asset } = getImageTexture(this.webgl, this.assetManager, props.source, errored => { + if (this.image) this.image.loaded = !errored; + onload?.(true); + }); + this.image = { texture, props: { ...props }, asset, loaded: false }; + ValueCell.update(this.renderable.values.tImage, texture); + this.renderable.update(); + } else { + onload?.(false); + } + if (!this.image) return; + + ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity); + ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation); + ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness); + ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, props.coverage === 'viewport' ? true : false); + ValueCell.updateIfChanged(this.renderable.values.dVariant, 'image'); + this.renderable.update(); + } + + private updateImageScaling() { + const v = this.renderable.values; + const [w, h] = v.uTexSize.ref.value; + const iw = this.image?.texture.getWidth() || 0; + const ih = this.image?.texture.getHeight() || 0; + const r = w / h; + const ir = iw / ih; + // responsive scaling with offset + if (r < ir) { + ValueCell.update(v.uImageScale, Vec2.set(v.uImageScale.ref.value, iw * h / ih, h)); + } else { + ValueCell.update(v.uImageScale, Vec2.set(v.uImageScale.ref.value, w, ih * w / iw)); + } + const [rw, rh] = v.uImageScale.ref.value; + const sr = rw / rh; + if (sr > r) { + ValueCell.update(v.uImageOffset, Vec2.set(v.uImageOffset.ref.value, (1 - r / sr) / 2, 0)); + } else { + ValueCell.update(v.uImageOffset, Vec2.set(v.uImageOffset.ref.value, 0, (1 - sr / r) / 2)); + } + } + + private updateGradient(colorA: Color, colorB: Color, ratio: number, variant: 'horizontalGradient' | 'radialGradient', viewportAdjusted: boolean) { + ValueCell.update(this.renderable.values.uGradientColorA, Color.toVec3Normalized(this.renderable.values.uGradientColorA.ref.value, colorA)); + ValueCell.update(this.renderable.values.uGradientColorB, Color.toVec3Normalized(this.renderable.values.uGradientColorB.ref.value, colorB)); + ValueCell.updateIfChanged(this.renderable.values.uGradientRatio, ratio); + ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, viewportAdjusted); + ValueCell.updateIfChanged(this.renderable.values.dVariant, variant); + this.renderable.update(); + } + + update(camera: ICamera, props: BackgroundProps, onload?: (changed: boolean) => void) { + if (props.variant.name === 'off') { + this.clearSkybox(); + this.clearImage(); + onload?.(false); + return; + } else if (props.variant.name === 'skybox') { + this.clearImage(); + this.updateSkybox(camera, props.variant.params, onload); + } else if (props.variant.name === 'image') { + this.clearSkybox(); + this.updateImage(props.variant.params, onload); + } else if (props.variant.name === 'horizontalGradient') { + this.clearSkybox(); + this.clearImage(); + this.updateGradient(props.variant.params.topColor, props.variant.params.bottomColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false); + onload?.(false); + } else if (props.variant.name === 'radialGradient') { + this.clearSkybox(); + this.clearImage(); + this.updateGradient(props.variant.params.centerColor, props.variant.params.edgeColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false); + onload?.(false); + } + + const { x, y, width, height } = camera.viewport; + ValueCell.update(this.renderable.values.uViewport, Vec4.set(this.renderable.values.uViewport.ref.value, x, y, width, height)); + } + + isEnabled(props: BackgroundProps) { + return !!( + (this.skybox && this.skybox.loaded) || + (this.image && this.image.loaded) || + props.variant.name === 'horizontalGradient' || + props.variant.name === 'radialGradient' + ); + } + + private isReady() { + return !!( + (this.skybox && this.skybox.loaded) || + (this.image && this.image.loaded) || + this.renderable.values.dVariant.ref.value === 'horizontalGradient' || + this.renderable.values.dVariant.ref.value === 'radialGradient' + ); + } + + render() { + if (!this.isReady()) return; + + if (this.renderable.values.dVariant.ref.value === 'image') { + this.updateImageScaling(); + } + + if (isTimingMode) this.webgl.timer.mark('BackgroundPass.render'); + this.renderable.render(); + if (isTimingMode) this.webgl.timer.markEnd('BackgroundPass.render'); + } + + dispose() { + this.clearSkybox(); + this.clearImage(); + } +} + +// + +const SkyboxName = 'background-skybox'; + +type CubeAssets = { [k in keyof CubeFaces]: Asset }; + +function getCubeAssets(assetManager: AssetManager, faces: SkyboxProps['faces']): CubeAssets { + if (faces.name === 'urls') { + return { + nx: Asset.getUrlAsset(assetManager, faces.params.nx), + ny: Asset.getUrlAsset(assetManager, faces.params.ny), + nz: Asset.getUrlAsset(assetManager, faces.params.nz), + px: Asset.getUrlAsset(assetManager, faces.params.px), + py: Asset.getUrlAsset(assetManager, faces.params.py), + pz: Asset.getUrlAsset(assetManager, faces.params.pz), + }; + } else { + return { + nx: faces.params.nx!, + ny: faces.params.ny!, + nz: faces.params.nz!, + px: faces.params.px!, + py: faces.params.py!, + pz: faces.params.pz!, + }; + } +} + +function getCubeFaces(assetManager: AssetManager, cubeAssets: CubeAssets): CubeFaces { + const resolve = (asset: Asset) => { + return assetManager.resolve(asset, 'binary').run().then(a => new Blob([a.data])); + }; + + return { + nx: resolve(cubeAssets.nx), + ny: resolve(cubeAssets.ny), + nz: resolve(cubeAssets.nz), + px: resolve(cubeAssets.px), + py: resolve(cubeAssets.py), + pz: resolve(cubeAssets.pz), + }; +} + +function getSkyboxHash(faces: SkyboxProps['faces']) { + if (faces.name === 'urls') { + return `${SkyboxName}_${faces.params.nx}|${faces.params.ny}|${faces.params.nz}|${faces.params.px}|${faces.params.py}|${faces.params.pz}`; + } else { + return `${SkyboxName}_${faces.params.nx?.id}|${faces.params.ny?.id}|${faces.params.nz?.id}|${faces.params.px?.id}|${faces.params.py?.id}|${faces.params.pz?.id}`; + } +} + +function areSkyboxTexturePropsEqual(facesA: SkyboxProps['faces'], facesB: SkyboxProps['faces']) { + return getSkyboxHash(facesA) === getSkyboxHash(facesB); +} + +function getSkyboxTexture(ctx: WebGLContext, assetManager: AssetManager, faces: SkyboxProps['faces'], onload?: (errored?: boolean) => void): { texture: Texture, assets: Asset[] } { + const cubeAssets = getCubeAssets(assetManager, faces); + const cubeFaces = getCubeFaces(assetManager, cubeAssets); + const assets = [cubeAssets.nx, cubeAssets.ny, cubeAssets.nz, cubeAssets.px, cubeAssets.py, cubeAssets.pz]; + const texture = ctx.resources.cubeTexture(cubeFaces, false, onload); + return { texture, assets }; +} + +// + +const ImageName = 'background-image'; + +function getImageHash(source: ImageProps['source']) { + if (source.name === 'url') { + return `${ImageName}_${source.params}`; + } else { + return `${ImageName}_${source.params?.id}`; + } +} + +function areImageTexturePropsEqual(sourceA: ImageProps['source'], sourceB: ImageProps['source']) { + return getImageHash(sourceA) === getImageHash(sourceB); +} + +function getImageTexture(ctx: WebGLContext, assetManager: AssetManager, source: ImageProps['source'], onload?: (errored?: boolean) => void): { texture: Texture, asset: Asset } { + const texture = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear'); + const img = new Image(); + img.onload = () => { + texture.load(img); + onload?.(); + }; + img.onerror = () => { + onload?.(true); + }; + const asset = source.name === 'url' + ? Asset.getUrlAsset(assetManager, source.params) + : source.params!; + assetManager.resolve(asset, 'binary').run().then(a => { + const blob = new Blob([a.data]); + img.src = URL.createObjectURL(blob); + }); + return { texture, asset }; +} + +// + +const BackgroundSchema = { + drawCount: ValueSpec('number'), + instanceCount: ValueSpec('number'), + aPosition: AttributeSpec('float32', 2, 0), + tSkybox: TextureSpec('texture', 'rgba', 'ubyte', 'linear'), + tImage: TextureSpec('texture', 'rgba', 'ubyte', 'linear'), + uImageScale: UniformSpec('v2'), + uImageOffset: UniformSpec('v2'), + uTexSize: UniformSpec('v2'), + uViewport: UniformSpec('v4'), + uViewportAdjusted: UniformSpec('b'), + uViewDirectionProjectionInverse: UniformSpec('m4'), + uGradientColorA: UniformSpec('v3'), + uGradientColorB: UniformSpec('v3'), + uGradientRatio: UniformSpec('f'), + uOpacity: UniformSpec('f'), + uSaturation: UniformSpec('f'), + uLightness: UniformSpec('f'), + dVariant: DefineSpec('string', ['skybox', 'image', 'verticalGradient', 'horizontalGradient', 'radialGradient']), +}; +const SkyboxShaderCode = ShaderCode('background', background_vert, background_frag); +type BackgroundRenderable = ComputeRenderable<Values<typeof BackgroundSchema>> + +function getBackgroundRenderable(ctx: WebGLContext, width: number, height: number): BackgroundRenderable { + const values: Values<typeof BackgroundSchema> = { + drawCount: ValueCell.create(6), + instanceCount: ValueCell.create(1), + aPosition: ValueCell.create(QuadPositions), + tSkybox: ValueCell.create(createNullTexture()), + tImage: ValueCell.create(createNullTexture()), + uImageScale: ValueCell.create(Vec2()), + uImageOffset: ValueCell.create(Vec2()), + uTexSize: ValueCell.create(Vec2.create(width, height)), + uViewport: ValueCell.create(Vec4()), + uViewportAdjusted: ValueCell.create(true), + uViewDirectionProjectionInverse: ValueCell.create(Mat4()), + uGradientColorA: ValueCell.create(Vec3()), + uGradientColorB: ValueCell.create(Vec3()), + uGradientRatio: ValueCell.create(0.5), + uOpacity: ValueCell.create(1), + uSaturation: ValueCell.create(0), + uLightness: ValueCell.create(0), + dVariant: ValueCell.create('skybox'), + }; + + const schema = { ...BackgroundSchema }; + const renderItem = createComputeRenderItem(ctx, 'triangles', SkyboxShaderCode, schema, values); + + return createComputeRenderable(renderItem, values); +} diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts index 0bb002d84c9df136b5bb8ce43025f07b8b583652..8b1d82c088a35b364fca04340d3811b55dbb5c9c 100644 --- a/src/mol-canvas3d/passes/draw.ts +++ b/src/mol-canvas3d/passes/draw.ts @@ -21,10 +21,11 @@ import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './pos import { MarkingPass, MarkingProps } from './marking'; import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util'; import { isTimingMode } from '../../mol-util/debug'; +import { AssetManager } from '../../mol-util/assets'; type Props = { - postprocessing: PostprocessingProps - marking: MarkingProps + postprocessing: PostprocessingProps; + marking: MarkingProps; transparentBackground: boolean; } @@ -50,7 +51,7 @@ export class DrawPass { private copyFboTarget: CopyRenderable; private copyFboPostprocessing: CopyRenderable; - private wboit: WboitPass | undefined; + private readonly wboit: WboitPass | undefined; private readonly marking: MarkingPass; readonly postprocessing: PostprocessingPass; private readonly antialiasing: AntialiasingPass; @@ -59,11 +60,10 @@ export class DrawPass { return !!this.wboit?.supported; } - constructor(private webgl: WebGLContext, width: number, height: number, enableWboit: boolean) { + constructor(private webgl: WebGLContext, assetManager: AssetManager, width: number, height: number, enableWboit: boolean) { const { extensions, resources, isWebGL2 } = webgl; this.drawTarget = createNullRenderTarget(webgl.gl); - this.colorTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'linear'); this.packedDepth = !extensions.depthTexture; @@ -79,7 +79,7 @@ export class DrawPass { this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined; this.marking = new MarkingPass(webgl, width, height); - this.postprocessing = new PostprocessingPass(webgl, this); + this.postprocessing = new PostprocessingPass(webgl, assetManager, this); this.antialiasing = new AntialiasingPass(webgl, this); this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture); @@ -120,14 +120,13 @@ export class DrawPass { private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, transparentBackground: boolean, postprocessingProps: PostprocessingProps) { if (!this.wboit?.supported) throw new Error('expected wboit to be supported'); - this.colorTarget.bind(); + this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth'); renderer.clear(true); // render opaque primitives - this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth'); - this.colorTarget.bind(); - renderer.clearDepth(); - renderer.renderWboitOpaque(scene.primitives, camera, null); + if (scene.hasOpaque) { + renderer.renderWboitOpaque(scene.primitives, camera, null); + } if (PostprocessingPass.isEnabled(postprocessingProps)) { if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) { @@ -165,14 +164,17 @@ export class DrawPass { if (toDrawingBuffer) { this.drawTarget.bind(); } else { - this.colorTarget.bind(); if (!this.packedDepth) { this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth'); + } else { + this.colorTarget.bind(); } } renderer.clear(true); - renderer.renderBlendedOpaque(scene.primitives, camera, null); + if (scene.hasOpaque) { + renderer.renderBlendedOpaque(scene.primitives, camera, null); + } if (!toDrawingBuffer) { // do a depth pass if not rendering to drawing buffer and @@ -235,7 +237,7 @@ export class DrawPass { } } - private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, props: Props) { + private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) { const volumeRendering = scene.volumes.renderables.length > 0; const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing); const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing); @@ -245,54 +247,52 @@ export class DrawPass { renderer.setViewport(x, y, width, height); renderer.update(camera); - if (props.transparentBackground && !antialiasingEnabled && toDrawingBuffer) { + if (transparentBackground && !antialiasingEnabled && toDrawingBuffer) { this.drawTarget.bind(); renderer.clear(false); } if (this.wboitEnabled) { - this._renderWboit(renderer, camera, scene, props.transparentBackground, props.postprocessing); - } else { - this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, props.transparentBackground, props.postprocessing); - } - - if (postprocessingEnabled) { - this.postprocessing.target.bind(); - } else if (!toDrawingBuffer || volumeRendering || this.wboitEnabled) { - this.colorTarget.bind(); + this._renderWboit(renderer, camera, scene, transparentBackground, props.postprocessing); } else { - this.drawTarget.bind(); + this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, transparentBackground, props.postprocessing); } - if (markingEnabled) { - if (scene.markerAverage > 0) { - const markingDepthTest = props.marking.ghostEdgeStrength < 1; - if (markingDepthTest && scene.markerAverage !== 1) { - this.marking.depthTarget.bind(); - renderer.clear(false, true); - renderer.renderMarkingDepth(scene.primitives, camera, null); - } + const target = postprocessingEnabled + ? this.postprocessing.target + : !toDrawingBuffer || volumeRendering || this.wboitEnabled + ? this.colorTarget + : this.drawTarget; - this.marking.maskTarget.bind(); + if (markingEnabled && scene.markerAverage > 0) { + const markingDepthTest = props.marking.ghostEdgeStrength < 1; + if (markingDepthTest && scene.markerAverage !== 1) { + this.marking.depthTarget.bind(); renderer.clear(false, true); - 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); + 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); + + this.marking.update(props.marking); + this.marking.render(camera.viewport, target); + } else { + target.bind(); } if (helper.debug.isEnabled) { helper.debug.syncVisibility(); - renderer.renderBlended(helper.debug.scene, camera, null); + renderer.renderBlended(helper.debug.scene, camera); } if (helper.handle.isEnabled) { - renderer.renderBlended(helper.handle.scene, camera, null); + renderer.renderBlended(helper.handle.scene, camera); } if (helper.camera.isEnabled) { helper.camera.update(camera); renderer.update(helper.camera.camera); - renderer.renderBlended(helper.camera.scene, helper.camera.camera, null); + renderer.renderBlended(helper.camera.scene, helper.camera.camera); } if (antialiasingEnabled) { @@ -314,15 +314,19 @@ export class DrawPass { render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) { if (isTimingMode) this.webgl.timer.mark('DrawPass.render'); const { renderer, camera, scene, helper } = ctx; - renderer.setTransparentBackground(props.transparentBackground); + + this.postprocessing.setTransparentBackground(props.transparentBackground); + const transparentBackground = props.transparentBackground || this.postprocessing.background.isEnabled(props.postprocessing.background); + + renderer.setTransparentBackground(transparentBackground); renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight()); renderer.setPixelRatio(this.webgl.pixelRatio); if (StereoCamera.is(camera)) { - this._render(renderer, camera.left, scene, helper, toDrawingBuffer, props); - this._render(renderer, camera.right, scene, helper, toDrawingBuffer, props); + this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, props); + this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, props); } else { - this._render(renderer, camera, scene, helper, toDrawingBuffer, props); + this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props); } if (isTimingMode) this.webgl.timer.markEnd('DrawPass.render'); } diff --git a/src/mol-canvas3d/passes/fxaa.ts b/src/mol-canvas3d/passes/fxaa.ts index ff1a0e878775ca6a898c0983382e10ac834c9091..bbb02430284d72c25be9f93b76bf863970e018ee 100644 --- a/src/mol-canvas3d/passes/fxaa.ts +++ b/src/mol-canvas3d/passes/fxaa.ts @@ -44,8 +44,8 @@ export class FxaaPass { state.depthMask(false); const { x, y, width, height } = viewport; - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); state.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); diff --git a/src/mol-canvas3d/passes/image.ts b/src/mol-canvas3d/passes/image.ts index 68acfa4c4b6977b36669d0b3a7afdbc6b01b96a2..e78aca0a31a447c5aa0f874dbf1995a13304c026 100644 --- a/src/mol-canvas3d/passes/image.ts +++ b/src/mol-canvas3d/passes/image.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> */ @@ -18,6 +18,7 @@ import { PixelData } from '../../mol-util/image'; import { Helper } from '../helper/helper'; import { CameraHelper, CameraHelperParams } from '../helper/camera-helper'; import { MarkingParams } from './marking'; +import { AssetManager } from '../../mol-util/assets'; export const ImageParams = { transparentBackground: PD.Boolean(false), @@ -47,10 +48,10 @@ export class ImagePass { get width() { return this._width; } get height() { return this._height; } - constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) { + constructor(private webgl: WebGLContext, assetManager: AssetManager, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) { this.props = { ...PD.getDefaultValues(ImageParams), ...props }; - this.drawPass = new DrawPass(webgl, 128, 128, enableWboit); + this.drawPass = new DrawPass(webgl, assetManager, 128, 128, enableWboit); this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass); this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass); @@ -63,6 +64,14 @@ export class ImagePass { this.setSize(1024, 768); } + updateBackground() { + return new Promise<void>(resolve => { + this.drawPass.postprocessing.background.update(this.camera, this.props.postprocessing.background, () => { + resolve(); + }); + }); + } + setSize(width: number, height: number) { if (width === this._width && height === this._height) return; diff --git a/src/mol-canvas3d/passes/marking.ts b/src/mol-canvas3d/passes/marking.ts index 73cde519fa611d2160c7a2c1788c5120146cf1e4..2093b5f2d1e2d0f818fbdd6519c2021f7654218d 100644 --- a/src/mol-canvas3d/passes/marking.ts +++ b/src/mol-canvas3d/passes/marking.ts @@ -64,8 +64,8 @@ export class MarkingPass { state.depthMask(false); const { x, y, width, height } = viewport; - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); state.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); @@ -82,8 +82,8 @@ export class MarkingPass { state.depthMask(false); const { x, y, width, height } = viewport; - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); } setSize(width: number, height: number) { diff --git a/src/mol-canvas3d/passes/multi-sample.ts b/src/mol-canvas3d/passes/multi-sample.ts index 82c861372d1c7677531d3861298695e764014da4..2137592b6ea329ea5d705e5cf13d3d162b14eecc 100644 --- a/src/mol-canvas3d/passes/multi-sample.ts +++ b/src/mol-canvas3d/passes/multi-sample.ts @@ -176,8 +176,8 @@ export class MultiSamplePass { state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); state.disable(gl.DEPTH_TEST); state.depthMask(false); - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); if (i === 0) { state.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); @@ -192,8 +192,8 @@ export class MultiSamplePass { compose.update(); this.bindOutputTarget(toDrawingBuffer); - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); state.disable(gl.BLEND); compose.render(); @@ -231,8 +231,8 @@ export class MultiSamplePass { state.disable(gl.BLEND); state.disable(gl.DEPTH_TEST); state.depthMask(false); - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); compose.render(); sampleIndex += 1; } else { @@ -267,8 +267,8 @@ export class MultiSamplePass { state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); state.disable(gl.DEPTH_TEST); state.depthMask(false); - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); if (sampleIndex === 0) { state.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); @@ -283,8 +283,8 @@ export class MultiSamplePass { drawPass.postprocessing.setOcclusionOffset(0, 0); this.bindOutputTarget(toDrawingBuffer); - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); const accumulationWeight = sampleIndex * sampleWeight; if (accumulationWeight > 0) { diff --git a/src/mol-canvas3d/passes/passes.ts b/src/mol-canvas3d/passes/passes.ts index 208795e33bb2af60d8966f2f857fb87dc594087a..117bb6ef0526f68074b40bf08b0f437d88c5ba88 100644 --- a/src/mol-canvas3d/passes/passes.ts +++ b/src/mol-canvas3d/passes/passes.ts @@ -8,15 +8,16 @@ import { DrawPass } from './draw'; import { PickPass } from './pick'; import { MultiSamplePass } from './multi-sample'; import { WebGLContext } from '../../mol-gl/webgl/context'; +import { AssetManager } from '../../mol-util/assets'; export class Passes { readonly draw: DrawPass; readonly pick: PickPass; readonly multiSample: MultiSamplePass; - constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) { + constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) { const { gl } = webgl; - this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false); + this.draw = new DrawPass(webgl, assetManager, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false); this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25); this.multiSample = new MultiSamplePass(webgl, this.draw); } diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts index fd41b8abe62071ec6a85734b6db554753e1ed03d..591517976863a47013460eb3158cfeeb699619a4 100644 --- a/src/mol-canvas3d/passes/postprocessing.ts +++ b/src/mol-canvas3d/passes/postprocessing.ts @@ -28,6 +28,8 @@ import { Color } from '../../mol-util/color'; import { FxaaParams, FxaaPass } from './fxaa'; import { SmaaParams, SmaaPass } from './smaa'; import { isTimingMode } from '../../mol-util/debug'; +import { BackgroundParams, BackgroundPass } from './background'; +import { AssetManager } from '../../mol-util/assets'; const OutlinesSchema = { ...QuadSchema, @@ -91,7 +93,7 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender ...QuadValues, tDepth: ValueCell.create(depthTexture), - uSamples: ValueCell.create([0.0, 0.0, 1.0]), + uSamples: ValueCell.create(getSamples(32)), dNSamples: ValueCell.create(32), uProjection: ValueCell.create(Mat4.identity()), @@ -138,7 +140,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir tSsaoDepth: ValueCell.create(ssaoDepthTexture), uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())), - uKernel: ValueCell.create([0.0]), + uKernel: ValueCell.create(getBlurKernel(15)), dOcclusionKernelSize: ValueCell.create(15), uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0), @@ -171,15 +173,26 @@ function getBlurKernel(kernelSize: number): number[] { return kernel; } -function getSamples(vectorSamples: Vec3[], nSamples: number): number[] { +const RandomHemisphereVector: Vec3[] = []; +for (let i = 0; i < 256; i++) { + const v = Vec3(); + v[0] = Math.random() * 2.0 - 1.0; + v[1] = Math.random() * 2.0 - 1.0; + v[2] = Math.random(); + Vec3.normalize(v, v); + Vec3.scale(v, v, Math.random()); + RandomHemisphereVector.push(v); +} + +function getSamples(nSamples: number): number[] { const samples = []; for (let i = 0; i < nSamples; i++) { let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples); scale = 0.1 + scale * (1.0 - 0.1); - samples.push(vectorSamples[i][0] * scale); - samples.push(vectorSamples[i][1] * scale); - samples.push(vectorSamples[i][2] * scale); + samples.push(RandomHemisphereVector[i][0] * scale); + samples.push(RandomHemisphereVector[i][1] * scale); + samples.push(RandomHemisphereVector[i][2] * scale); } return samples; @@ -274,12 +287,13 @@ export const PostprocessingParams = { smaa: PD.Group(SmaaParams), off: PD.Group({}) }, { 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'; + return props.occlusion.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off'; } static isOutlineEnabled(props: PostprocessingProps) { @@ -291,7 +305,6 @@ export class PostprocessingPass { private readonly outlinesTarget: RenderTarget; private readonly outlinesRenderable: OutlinesRenderable; - private readonly randomHemisphereVector: Vec3[]; private readonly ssaoFramebuffer: Framebuffer; private readonly ssaoBlurFirstPassFramebuffer: Framebuffer; private readonly ssaoBlurSecondPassFramebuffer: Framebuffer; @@ -318,7 +331,10 @@ export class PostprocessingPass { return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor; } - constructor(private webgl: WebGLContext, private drawPass: DrawPass) { + private readonly bgColor = Vec3(); + readonly background: BackgroundPass; + + constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) { const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass; const width = colorTarget.getWidth(); const height = colorTarget.getHeight(); @@ -334,16 +350,6 @@ export class PostprocessingPass { this.outlinesTarget = webgl.createRenderTarget(width, height, false); this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent); - this.randomHemisphereVector = []; - for (let i = 0; i < 256; i++) { - const v = Vec3(); - v[0] = Math.random() * 2.0 - 1.0; - v[1] = Math.random() * 2.0 - 1.0; - v[2] = Math.random(); - Vec3.normalize(v, v); - Vec3.scale(v, v, Math.random()); - this.randomHemisphereVector.push(v); - } this.ssaoFramebuffer = webgl.resources.framebuffer(); this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer(); this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer(); @@ -368,6 +374,8 @@ export class PostprocessingPass { 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.background = new BackgroundPass(webgl, assetManager, width, height); } setSize(width: number, height: number) { @@ -391,6 +399,8 @@ export class PostprocessingPass { 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)); + + this.background.setSize(width, height); } } @@ -440,7 +450,7 @@ export class PostprocessingPass { needsUpdateSsao = true; this.nSamples = props.occlusion.params.samples; - ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples)); + 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)); @@ -538,8 +548,8 @@ export class PostprocessingPass { state.depthMask(false); const { x, y, width, height } = camera.viewport; - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); } private occlusionOffset: [x: number, y: number] = [0, 0]; @@ -549,6 +559,11 @@ export class PostprocessingPass { ValueCell.update(this.renderable.values.uOcclusionOffset, Vec2.set(this.renderable.values.uOcclusionOffset.ref.value, x, y)); } + private transparentBackground = false; + setTransparentBackground(value: boolean) { + this.transparentBackground = value; + } + render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) { if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render'); this.updateState(camera, transparentBackground, backgroundColor, props); @@ -583,8 +598,23 @@ export class PostprocessingPass { } const { gl, state } = this.webgl; - state.clearColor(0, 0, 0, 1); - gl.clear(gl.COLOR_BUFFER_BIT); + + this.background.update(camera, props.background); + if (this.background.isEnabled(props.background)) { + if (this.transparentBackground) { + state.clearColor(0, 0, 0, 0); + } else { + Color.toVec3Normalized(this.bgColor, backgroundColor); + state.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], 1); + } + gl.clear(gl.COLOR_BUFFER_BIT); + state.enable(gl.BLEND); + state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + this.background.render(); + } else { + state.clearColor(0, 0, 0, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + } this.renderable.render(); if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render'); diff --git a/src/mol-canvas3d/passes/smaa.ts b/src/mol-canvas3d/passes/smaa.ts index 3002b2ff33f3b4bd244675dc264ec4aa49f30c36..4ac7296fa717dcb72e3c790fd027275c6a5177b5 100644 --- a/src/mol-canvas3d/passes/smaa.ts +++ b/src/mol-canvas3d/passes/smaa.ts @@ -71,8 +71,8 @@ export class SmaaPass { state.depthMask(false); const { x, y, width, height } = viewport; - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); state.clearColor(0, 0, 0, 1); gl.clear(gl.COLOR_BUFFER_BIT); diff --git a/src/mol-geo/geometry/texture-mesh/color-smoothing.ts b/src/mol-geo/geometry/texture-mesh/color-smoothing.ts index 0a0a1786c5d2ae9412e09f698b33db3be74f7aea..6a9983a8f45c4556617cc63cb650f28bc6442b34 100644 --- a/src/mol-geo/geometry/texture-mesh/color-smoothing.ts +++ b/src/mol-geo/geometry/texture-mesh/color-smoothing.ts @@ -319,8 +319,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu if (isTimingMode) webgl.timer.mark('ColorAccumulate.render'); setAccumulateDefaults(webgl); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); + state.viewport(0, 0, width, height); + state.scissor(0, 0, width, height); gl.clear(gl.COLOR_BUFFER_BIT); ValueCell.update(uCurrentY, 0); let currCol = 0; @@ -336,8 +336,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu // console.log({ i, currX, currY }); ValueCell.update(uCurrentX, currX); ValueCell.update(uCurrentSlice, i); - gl.viewport(currX, currY, dx, dy); - gl.scissor(currX, currY, dx, dy); + state.viewport(currX, currY, dx, dy); + state.scissor(currX, currY, dx, dy); accumulateRenderable.render(); ++currCol; currX += dx; @@ -371,8 +371,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu setNormalizeDefaults(webgl); texture.attachFramebuffer(framebuffer, 0); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); + state.viewport(0, 0, width, height); + state.scissor(0, 0, width, height); gl.clear(gl.COLOR_BUFFER_BIT); normalizeRenderable.render(); if (isTimingMode) webgl.timer.markEnd('ColorNormalize.render'); diff --git a/src/mol-gl/compute/grid3d.ts b/src/mol-gl/compute/grid3d.ts index e291661ccae12a921b5d127ce6ecdbbc22617ce5..6a4922e34a6dcc497fdb5f672d94318fe0afe601 100644 --- a/src/mol-gl/compute/grid3d.ts +++ b/src/mol-gl/compute/grid3d.ts @@ -225,8 +225,8 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS> function resetGl(webgl: WebGLContext, w: number) { const { gl, state } = webgl; - gl.viewport(0, 0, w, w); - gl.scissor(0, 0, w, w); + state.viewport(0, 0, w, w); + state.scissor(0, 0, w, w); state.disable(gl.SCISSOR_TEST); state.disable(gl.BLEND); state.disable(gl.DEPTH_TEST); diff --git a/src/mol-gl/compute/histogram-pyramid/reduction.ts b/src/mol-gl/compute/histogram-pyramid/reduction.ts index 8d162f5e99e8e69cf1f5fcb188a956336682d59f..b95ad7c44933a9398687654794311b74b52e8ddf 100644 --- a/src/mol-gl/compute/histogram-pyramid/reduction.ts +++ b/src/mol-gl/compute/histogram-pyramid/reduction.ts @@ -122,7 +122,7 @@ export interface HistogramPyramid { export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2, gridTexDim: Vec3): HistogramPyramid { if (isTimingMode) ctx.timer.mark('createHistogramPyramid'); - const { gl } = ctx; + const { gl, state } = ctx; const w = inputTexture.getWidth(); const h = inputTexture.getHeight(); @@ -146,7 +146,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, const framebuffer = getFramebuffer('pyramid', ctx); pyramidTex.attachFramebuffer(framebuffer, 0); - gl.viewport(0, 0, maxSizeX, maxSizeY); + state.viewport(0, 0, maxSizeX, maxSizeY); if (isWebGL2(gl)) { gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]); } else { @@ -157,7 +157,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i)); const renderable = getHistopyramidReductionRenderable(ctx, inputTexture, levelTexturesFramebuffers[0].texture); - ctx.state.currentRenderItemId = -1; + state.currentRenderItemId = -1; setRenderingDefaults(ctx); let offset = 0; @@ -176,15 +176,15 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture); renderable.update(); } - ctx.state.currentRenderItemId = -1; - gl.viewport(0, 0, size, size); - gl.scissor(0, 0, size, size); + state.currentRenderItemId = -1; + state.viewport(0, 0, size, size); + state.scissor(0, 0, size, size); if (isWebGL2(gl)) { gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]); } else { gl.clear(gl.COLOR_BUFFER_BIT); } - gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]); + state.scissor(0, 0, gridTexDim[0], gridTexDim[1]); renderable.render(); pyramidTex.bind(0); diff --git a/src/mol-gl/compute/histogram-pyramid/sum.ts b/src/mol-gl/compute/histogram-pyramid/sum.ts index a1cd5919a7bf5632b87d6ca0c8ea274ecff1caf4..65c36515d80ac9d2f1f7e3e87118c90fe240bcaf 100644 --- a/src/mol-gl/compute/histogram-pyramid/sum.ts +++ b/src/mol-gl/compute/histogram-pyramid/sum.ts @@ -68,7 +68,7 @@ const sumInts = new Int32Array(4); export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) { if (isTimingMode) ctx.timer.mark('getHistopyramidSum'); - const { gl, resources } = ctx; + const { gl, state, resources } = ctx; const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture); ctx.state.currentRenderItemId = -1; @@ -89,7 +89,7 @@ export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture setRenderingDefaults(ctx); - gl.viewport(0, 0, 1, 1); + state.viewport(0, 0, 1, 1); renderable.render(); gl.finish(); diff --git a/src/mol-gl/compute/marching-cubes/active-voxels.ts b/src/mol-gl/compute/marching-cubes/active-voxels.ts index b16014c011b8eef48a74c800b69c595b463bdee7..c460512d509d791b3ebfc5eba5d07afefa7f32ef 100644 --- a/src/mol-gl/compute/marching-cubes/active-voxels.ts +++ b/src/mol-gl/compute/marching-cubes/active-voxels.ts @@ -85,7 +85,7 @@ function setRenderingDefaults(ctx: WebGLContext) { export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) { if (isTimingMode) ctx.timer.mark('calcActiveVoxels'); - const { gl, resources } = ctx; + const { gl, state, resources } = ctx; const width = volumeData.getWidth(); const height = volumeData.getHeight(); @@ -106,10 +106,10 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim activeVoxelsTex.attachFramebuffer(framebuffer, 0); setRenderingDefaults(ctx); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); + state.viewport(0, 0, width, height); + state.scissor(0, 0, width, height); gl.clear(gl.COLOR_BUFFER_BIT); - gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]); + state.scissor(0, 0, gridTexDim[0], gridTexDim[1]); renderable.render(); // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim); diff --git a/src/mol-gl/compute/marching-cubes/isosurface.ts b/src/mol-gl/compute/marching-cubes/isosurface.ts index 3c628b25554fbd79c14f59fee00d2fbf236c3c7c..0215937e512ad4839c8b5dc9fb9b16fe1cf044e1 100644 --- a/src/mol-gl/compute/marching-cubes/isosurface.ts +++ b/src/mol-gl/compute/marching-cubes/isosurface.ts @@ -127,7 +127,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex if (!drawBuffers) throw new Error('need WebGL draw buffers'); if (isTimingMode) ctx.timer.mark('createIsosurfaceBuffers'); - const { gl, resources, extensions } = ctx; + const { gl, state, resources, extensions } = ctx; const { pyramidTex, height, levels, scale, count } = histogramPyramid; const width = pyramidTex.getWidth(); @@ -192,7 +192,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex ]); setRenderingDefaults(ctx); - gl.viewport(0, 0, width, height); + state.viewport(0, 0, width, height); gl.clear(gl.COLOR_BUFFER_BIT); renderable.render(); diff --git a/src/mol-gl/compute/util.ts b/src/mol-gl/compute/util.ts index 2e759efbb3def192fe627db0c448a0b81ba39bc5..bfef65236a7a6ee14392f4f0f50f2fc1da56cac0 100644 --- a/src/mol-gl/compute/util.ts +++ b/src/mol-gl/compute/util.ts @@ -125,8 +125,8 @@ export function readAlphaTexture(ctx: WebGLContext, texture: Texture) { state.clearColor(0, 0, 0, 0); state.blendFunc(gl.ONE, gl.ONE); state.blendEquation(gl.FUNC_ADD); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); + state.viewport(0, 0, width, height); + state.scissor(0, 0, width, height); gl.clear(gl.COLOR_BUFFER_BIT); copy.render(); diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index 259e99e16651bed9b82cced4f10bcc610e0d3e4d..2182662f3675347955f025390e9892d948904ab4 100644 --- a/src/mol-gl/renderer.ts +++ b/src/mol-gl/renderer.ts @@ -64,7 +64,7 @@ interface Renderer { renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void - renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void + renderBlended: (group: Scene, camera: ICamera) => void renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void @@ -359,8 +359,8 @@ namespace Renderer { state.colorMask(true, true, true, true); const { x, y, width, height } = viewport; - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); globalUniformsNeedUpdate = true; state.currentRenderItemId = -1; @@ -475,9 +475,13 @@ namespace Renderer { if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingMask'); }; - const renderBlended = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => { - renderBlendedOpaque(group, camera, depthTexture); - renderBlendedTransparent(group, camera, depthTexture); + const renderBlended = (scene: Scene, camera: ICamera) => { + if (scene.hasOpaque) { + renderBlendedOpaque(scene, camera, null); + } + if (scene.opacityAverage < 1) { + renderBlendedTransparent(scene, camera, null); + } }; const renderBlendedOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => { @@ -591,7 +595,7 @@ namespace Renderer { // TODO: simplify, handle in renderable.state??? // uAlpha is updated in "render" so we need to recompute it here const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1); - if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) { + if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || r.values.dXrayShaded?.ref.value) { renderObject(r, 'colorWboit', Flag.None); } } @@ -714,8 +718,8 @@ namespace Renderer { } }, setViewport: (x: number, y: number, width: number, height: number) => { - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); + state.viewport(x, y, width, height); + state.scissor(x, y, width, height); if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) { Viewport.set(viewport, x, y, width, height); ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height)); diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts index 21f8cd529798cfaf739f8ae3249e5b14ab289cf7..a46be8ae129f3be7d75e5dd6e821181757923386 100644 --- a/src/mol-gl/scene.ts +++ b/src/mol-gl/scene.ts @@ -80,8 +80,12 @@ interface Scene extends Object3D { has: (o: GraphicsRenderObject) => boolean clear: () => void forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void + /** Marker average of primitive renderables */ readonly markerAverage: number + /** Opacity average of primitive renderables */ readonly opacityAverage: number + /** Is `true` if any primitive renderable (possibly) has any opaque part */ + readonly hasOpaque: boolean } namespace Scene { @@ -103,6 +107,7 @@ namespace Scene { let markerAverage = 0; let opacityAverage = 0; + let hasOpaque = false; const object3d = Object3D.create(); const { view, position, direction, up } = object3d; @@ -160,7 +165,9 @@ namespace Scene { } renderables.sort(renderableSort); + markerAverage = calculateMarkerAverage(); opacityAverage = calculateOpacityAverage(); + hasOpaque = calculateHasOpaque(); return true; } @@ -182,7 +189,10 @@ namespace Scene { const newVisibleHash = computeVisibleHash(); if (newVisibleHash !== visibleHash) { boundingSphereVisibleDirty = true; + markerAverage = calculateMarkerAverage(); opacityAverage = calculateOpacityAverage(); + hasOpaque = calculateHasOpaque(); + visibleHash = newVisibleHash; return true; } else { return false; @@ -212,12 +222,27 @@ namespace Scene { // uAlpha is updated in "render" so we need to recompute it here const alpha = clamp(p.values.alpha.ref.value * p.state.alphaFactor, 0, 1); const xray = p.values.dXrayShaded?.ref.value ? 0.5 : 1; - opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray; + const fuzzy = p.values.dPointStyle?.ref.value === 'fuzzy' ? 0.5 : 1; + const text = p.values.dGeometryType.ref.value === 'text' ? 0.5 : 1; + opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray * fuzzy * text; count += 1; } return count > 0 ? opacityAverage / count : 0; } + function calculateHasOpaque() { + if (primitives.length === 0) return false; + for (let i = 0, il = primitives.length; i < il; ++i) { + const p = primitives[i]; + if (!p.state.visible) continue; + + if (p.state.opaque) return true; + if (p.state.alphaFactor === 1 && p.values.alpha.ref.value === 1 && p.values.transparencyAverage.ref.value !== 1) return true; + if (p.values.dTransparentBackfaces?.ref.value === 'opaque') return true; + } + return false; + } + return { view, position, direction, up, @@ -245,6 +270,7 @@ namespace Scene { } markerAverage = calculateMarkerAverage(); opacityAverage = calculateOpacityAverage(); + hasOpaque = calculateHasOpaque(); }, add: (o: GraphicsRenderObject) => commitQueue.add(o), remove: (o: GraphicsRenderObject) => commitQueue.remove(o), @@ -281,7 +307,6 @@ namespace Scene { if (boundingSphereVisibleDirty) { calculateBoundingSphere(renderables, boundingSphereVisible, true); boundingSphereVisibleDirty = false; - visibleHash = computeVisibleHash(); } return boundingSphereVisible; }, @@ -291,6 +316,9 @@ namespace Scene { get opacityAverage() { return opacityAverage; }, + get hasOpaque() { + return hasOpaque; + }, }; } } diff --git a/src/mol-gl/shader-code.ts b/src/mol-gl/shader-code.ts index c65f6df5dd080bcee9075dfabf0275b990f479db..a99cf5b31cafaac10f2d9352c82d79b3f908efeb 100644 --- a/src/mol-gl/shader-code.ts +++ b/src/mol-gl/shader-code.ts @@ -292,7 +292,9 @@ const glsl300VertPrefixCommon = ` const glsl300FragPrefixCommon = ` #define varying in #define texture2D texture +#define textureCube texture #define texture2DLodEXT textureLod +#define textureCubeLodEXT textureLod #define gl_FragColor out_FragData0 #define gl_FragDepthEXT gl_FragDepth diff --git a/src/mol-gl/shader/background.frag.ts b/src/mol-gl/shader/background.frag.ts new file mode 100644 index 0000000000000000000000000000000000000000..a764a9ad8237ec635bb53ad45c7bb7e08f297c4f --- /dev/null +++ b/src/mol-gl/shader/background.frag.ts @@ -0,0 +1,85 @@ +export const background_frag = ` +precision mediump float; +precision mediump samplerCube; +precision mediump sampler2D; + +#if defined(dVariant_skybox) + uniform samplerCube tSkybox; + uniform mat4 uViewDirectionProjectionInverse; + uniform float uOpacity; + uniform float uSaturation; + uniform float uLightness; +#elif defined(dVariant_image) + uniform sampler2D tImage; + uniform vec2 uImageScale; + uniform vec2 uImageOffset; + uniform float uOpacity; + uniform float uSaturation; + uniform float uLightness; +#elif defined(dVariant_horizontalGradient) || defined(dVariant_radialGradient) + uniform vec3 uGradientColorA; + uniform vec3 uGradientColorB; + uniform float uGradientRatio; +#endif + +uniform vec2 uTexSize; +uniform vec4 uViewport; +uniform bool uViewportAdjusted; +varying vec4 vPosition; + +// TODO: add as general pp option to remove banding? +// Iestyn's RGB dither from http://alex.vlachos.com/graphics/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf +vec3 ScreenSpaceDither(vec2 vScreenPos) { + vec3 vDither = vec3(dot(vec2(171.0, 231.0), vScreenPos.xy)); + vDither.rgb = fract(vDither.rgb / vec3(103.0, 71.0, 97.0)); + return vDither.rgb / 255.0; +} + +vec3 saturateColor(vec3 c, float amount) { + // https://www.w3.org/TR/WCAG21/#dfn-relative-luminance + const vec3 W = vec3(0.2125, 0.7154, 0.0721); + vec3 intensity = vec3(dot(c, W)); + return mix(intensity, c, 1.0 + amount); +} + +vec3 lightenColor(vec3 c, float amount) { + return c + amount; +} + +void main() { + #if defined(dVariant_skybox) + vec4 t = uViewDirectionProjectionInverse * vPosition; + gl_FragColor = textureCube(tSkybox, normalize(t.xyz / t.w)); + gl_FragColor.a = uOpacity; + gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness); + #elif defined(dVariant_image) + vec2 coords; + if (uViewportAdjusted) { + coords = ((gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uImageScale) + uImageOffset; + } else { + coords = (gl_FragCoord.xy / uImageScale) + uImageOffset; + } + gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y)); + gl_FragColor.a = uOpacity; + gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness); + #elif defined(dVariant_horizontalGradient) + float d; + if (uViewportAdjusted) { + d = ((gl_FragCoord.y - uViewport.y) * (uTexSize.y / uViewport.w) / uTexSize.y) + 1.0 - (uGradientRatio * 2.0); + } else { + d = (gl_FragCoord.y / uTexSize.y) + 1.0 - (uGradientRatio * 2.0); + } + gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, clamp(d, 0.0, 1.0)), 1.0); + gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy); + #elif defined(dVariant_radialGradient) + float d; + if (uViewportAdjusted) { + d = distance(vec2(0.5), (gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uTexSize) + uGradientRatio - 0.5; + } else { + d = distance(vec2(0.5), gl_FragCoord.xy / uTexSize) + uGradientRatio - 0.5; + } + gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, 1.0 - clamp(d, 0.0, 1.0)), 1.0); + gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy); + #endif +} +`; diff --git a/src/mol-gl/shader/background.vert.ts b/src/mol-gl/shader/background.vert.ts new file mode 100644 index 0000000000000000000000000000000000000000..3f1b86fbbf861b84d577cd73c6e5e1a32908c114 --- /dev/null +++ b/src/mol-gl/shader/background.vert.ts @@ -0,0 +1,12 @@ +export const background_vert = ` +precision mediump float; + +attribute vec2 aPosition; + +varying vec4 vPosition; + +void main() { + vPosition = vec4(aPosition, 1.0, 1.0); + gl_Position = vec4(aPosition, 1.0, 1.0); +} +`; diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index c3cd962a4f42716371db39688dcd4c66cd46ac22..f8d399ae8c71206870a3429fea2b790538f4d96a 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -142,12 +142,12 @@ export function readPixels(gl: GLRenderingContext, x: number, y: number, width: if (isDebugMode) checkError(gl); } -function getDrawingBufferPixelData(gl: GLRenderingContext) { +function getDrawingBufferPixelData(gl: GLRenderingContext, state: WebGLState) { const w = gl.drawingBufferWidth; const h = gl.drawingBufferHeight; const buffer = new Uint8Array(w * h * 4); unbindFramebuffer(gl); - gl.viewport(0, 0, w, h); + state.viewport(0, 0, w, h); readPixels(gl, 0, 0, w, h, buffer); return PixelData.flipY(PixelData.create(buffer, w, h)); } @@ -164,6 +164,7 @@ function createStats() { renderbuffer: 0, shader: 0, texture: 0, + cubeTexture: 0, vertexArray: 0, }, @@ -345,15 +346,15 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal readPixelsAsync, waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl), waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl), - getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl), + getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl, state), clear: (red: number, green: number, blue: number, alpha: number) => { unbindFramebuffer(gl); state.enable(gl.SCISSOR_TEST); state.depthMask(true); state.colorMask(true, true, true, true); state.clearColor(red, green, blue, alpha); - gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - gl.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + state.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); + state.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); }, diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts index e05dced716b9d365cf241e6e0a3191b4f4a19d3c..83f044f37abc7f117685b24792e4a608f2455b85 100644 --- a/src/mol-gl/webgl/render-item.ts +++ b/src/mol-gl/webgl/render-item.ts @@ -150,8 +150,8 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode: vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null; } - let drawCount = values.drawCount.ref.value; - let instanceCount = values.instanceCount.ref.value; + let drawCount: number = values.drawCount.ref.value; + let instanceCount: number = values.instanceCount.ref.value; stats.drawCount += drawCount; stats.instanceCount += instanceCount; @@ -168,7 +168,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode: getProgram: (variant: T) => programs[variant], render: (variant: T, sharedTexturesCount: number) => { - if (drawCount === 0 || instanceCount === 0 || ctx.isContextLost) return; + if (drawCount === 0 || instanceCount === 0) return; const program = programs[variant]; if (program.id === currentProgramId && state.currentRenderItemId === id) { program.setUniforms(uniformValueEntries); diff --git a/src/mol-gl/webgl/resources.ts b/src/mol-gl/webgl/resources.ts index 4dd175cc7788bfdffd80614b8a56add7d2f7a336..3e2e2d370bfc771499cf8b5b7641f5ab25b2ce52 100644 --- a/src/mol-gl/webgl/resources.ts +++ b/src/mol-gl/webgl/resources.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> */ @@ -17,7 +17,7 @@ import { hashString, hashFnv32a } from '../../mol-data/util'; import { DefineValues, ShaderCode } from '../shader-code'; import { RenderableSchema } from '../renderable/schema'; import { createRenderbuffer, Renderbuffer, RenderbufferAttachment, RenderbufferFormat } from './renderbuffer'; -import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture } from './texture'; +import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture, CubeFaces, createCubeTexture } from './texture'; import { VertexArray, createVertexArray } from './vertex-array'; function defineValueHash(v: boolean | number | string): number { @@ -59,6 +59,7 @@ export interface WebGLResources { renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer shader: (type: ShaderType, source: string) => Shader texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture, + cubeTexture: (faces: CubeFaces, mipaps: boolean, onload?: () => void) => Texture, vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray, getByteCounts: () => ByteCounts @@ -76,6 +77,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats renderbuffer: new Set<Resource>(), shader: new Set<Resource>(), texture: new Set<Resource>(), + cubeTexture: new Set<Resource>(), vertexArray: new Set<Resource>(), }; @@ -137,6 +139,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => { return wrap('texture', createTexture(gl, extensions, kind, format, type, filter)); }, + cubeTexture: (faces: CubeFaces, mipmaps: boolean, onload?: () => void) => { + return wrap('cubeTexture', createCubeTexture(gl, faces, mipmaps, onload)); + }, vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => { return wrap('vertexArray', createVertexArray(gl, extensions, program, attributeBuffers, elementsBuffer)); }, @@ -146,6 +151,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats sets.texture.forEach(r => { texture += (r as Texture).getByteCount(); }); + sets.cubeTexture.forEach(r => { + texture += (r as Texture).getByteCount(); + }); let attribute = 0; sets.attribute.forEach(r => { diff --git a/src/mol-gl/webgl/state.ts b/src/mol-gl/webgl/state.ts index d84c91bc8fd48ed129b996d74dfdada51f7b958c..dc6184d7e894b8d274a4c111a1fc8355436b1e88 100644 --- a/src/mol-gl/webgl/state.ts +++ b/src/mol-gl/webgl/state.ts @@ -69,6 +69,9 @@ export type WebGLState = { clearVertexAttribsState: () => void disableUnusedVertexAttribs: () => void + viewport: (x: number, y: number, width: number, height: number) => void + scissor: (x: number, y: number, width: number, height: number) => void + reset: () => void } @@ -95,6 +98,9 @@ export function createState(gl: GLRenderingContext): WebGLState { let maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); const vertexAttribsState: number[] = []; + let currentViewport: [number, number, number, number] = gl.getParameter(gl.VIEWPORT); + let currentScissor: [number, number, number, number] = gl.getParameter(gl.SCISSOR_BOX); + const clearVertexAttribsState = () => { for (let i = 0; i < maxVertexAttribs; ++i) { vertexAttribsState[i] = 0; @@ -222,6 +228,26 @@ export function createState(gl: GLRenderingContext): WebGLState { } }, + viewport: (x: number, y: number, width: number, height: number) => { + if (x !== currentViewport[0] || y !== currentViewport[1] || width !== currentViewport[2] || height !== currentViewport[3]) { + gl.viewport(x, y, width, height); + currentViewport[0] = x; + currentViewport[1] = y; + currentViewport[2] = width; + currentViewport[3] = height; + } + }, + + scissor: (x: number, y: number, width: number, height: number) => { + if (x !== currentScissor[0] || y !== currentScissor[1] || width !== currentScissor[2] || height !== currentScissor[3]) { + gl.scissor(x, y, width, height); + currentScissor[0] = x; + currentScissor[1] = y; + currentScissor[2] = width; + currentScissor[3] = height; + } + }, + reset: () => { enabledCapabilities = {}; @@ -247,6 +273,9 @@ export function createState(gl: GLRenderingContext): WebGLState { for (let i = 0; i < maxVertexAttribs; ++i) { vertexAttribsState[i] = 0; } + + currentViewport = gl.getParameter(gl.VIEWPORT); + currentScissor = gl.getParameter(gl.SCISSOR_BOX); } }; } \ No newline at end of file diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts index 3966a268d5d2d96cc7abbc94c18013523b86e617..26d4a2f1d20b44b89a51bc821c552efda8131e7e 100644 --- a/src/mol-gl/webgl/texture.ts +++ b/src/mol-gl/webgl/texture.ts @@ -11,8 +11,9 @@ import { RenderableSchema } from '../renderable/schema'; import { idFactory } from '../../mol-util/id-factory'; import { Framebuffer } from './framebuffer'; import { isWebGL2, GLRenderingContext } from './compat'; -import { ValueOf } from '../../mol-util/type-helpers'; +import { isPromiseLike, ValueOf } from '../../mol-util/type-helpers'; import { WebGLExtensions } from './extensions'; +import { objectForEach } from '../../mol-util/object'; const getNextTextureId = idFactory(); @@ -423,6 +424,123 @@ export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture: // +export type CubeSide = 'nx' | 'ny' | 'nz' | 'px' | 'py' | 'pz'; + +export type CubeFaces = { + [k in CubeSide]: string | File | Promise<Blob>; +} + +export function getCubeTarget(gl: GLRenderingContext, side: CubeSide): number { + switch (side) { + case 'nx': return gl.TEXTURE_CUBE_MAP_NEGATIVE_X; + case 'ny': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Y; + case 'nz': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Z; + case 'px': return gl.TEXTURE_CUBE_MAP_POSITIVE_X; + case 'py': return gl.TEXTURE_CUBE_MAP_POSITIVE_Y; + case 'pz': return gl.TEXTURE_CUBE_MAP_POSITIVE_Z; + } +} + +export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, mipmaps: boolean, onload?: (errored?: boolean) => void): Texture { + const target = gl.TEXTURE_CUBE_MAP; + const filter = gl.LINEAR; + const internalFormat = gl.RGBA; + const format = gl.RGBA; + const type = gl.UNSIGNED_BYTE; + + let size = 0; + + const texture = gl.createTexture(); + gl.bindTexture(target, texture); + + let loadedCount = 0; + objectForEach(faces, (source, side) => { + if (!source) return; + + const level = 0; + const cubeTarget = getCubeTarget(gl, side as CubeSide); + + const image = new Image(); + if (source instanceof File) { + image.src = URL.createObjectURL(source); + } else if (isPromiseLike(source)) { + source.then(blob => { + image.src = URL.createObjectURL(blob); + }); + } else { + image.src = source; + } + image.addEventListener('load', () => { + if (size === 0) size = image.width; + + gl.texImage2D(cubeTarget, level, internalFormat, size, size, 0, format, type, null); + gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4); + gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE); + gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0); + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); + gl.bindTexture(target, texture); + gl.texImage2D(cubeTarget, level, internalFormat, format, type, image); + + loadedCount += 1; + if (loadedCount === 6) { + if (!destroyed) { + if (mipmaps) { + gl.generateMipmap(target); + gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); + } else { + gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter); + } + gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter); + } + onload?.(destroyed); + } + }); + image.addEventListener('error', () => { + onload?.(true); + }); + }); + + let destroyed = false; + + return { + id: getNextTextureId(), + target, + format, + internalFormat, + type, + filter, + + getWidth: () => size, + getHeight: () => size, + getDepth: () => 0, + getByteCount: () => { + return getByteCount('rgba', 'ubyte', size, size, 0) * 6 * (mipmaps ? 2 : 1); + }, + + define: () => {}, + load: () => {}, + bind: (id: TextureId) => { + gl.activeTexture(gl.TEXTURE0 + id); + gl.bindTexture(target, texture); + }, + unbind: (id: TextureId) => { + gl.activeTexture(gl.TEXTURE0 + id); + gl.bindTexture(target, null); + }, + attachFramebuffer: () => {}, + detachFramebuffer: () => {}, + + reset: () => {}, + destroy: () => { + if (destroyed) return; + gl.deleteTexture(texture); + destroyed = true; + }, + }; +} + +// + export function createNullTexture(gl?: GLRenderingContext): Texture { const target = gl?.TEXTURE_2D ?? 3553; return { diff --git a/src/mol-math/geometry/gaussian-density/gpu.ts b/src/mol-math/geometry/gaussian-density/gpu.ts index 97c5f0c861cd460b0ecf629bc1722aca4b8475a9..05a26567d0017bbabde11f392d26bca3ccb07afe 100644 --- a/src/mol-math/geometry/gaussian-density/gpu.ts +++ b/src/mol-math/geometry/gaussian-density/gpu.ts @@ -166,8 +166,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat state.currentRenderItemId = -1; fbTex.attachFramebuffer(framebuffer, 0); if (clear) { - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); + state.viewport(0, 0, width, height); + state.scissor(0, 0, width, height); gl.clear(gl.COLOR_BUFFER_BIT); } ValueCell.update(uCurrentY, 0); @@ -184,8 +184,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat // console.log({ i, currX, currY }); ValueCell.update(uCurrentX, currX); ValueCell.update(uCurrentSlice, i); - gl.viewport(currX, currY, dx, dy); - gl.scissor(currX, currY, dx, dy); + state.viewport(currX, currY, dx, dy); + state.scissor(currX, currY, dx, dy); renderable.render(); ++currCol; currX += dx; @@ -232,8 +232,8 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat const framebuffer = getFramebuffer(webgl); framebuffer.bind(); setRenderingDefaults(webgl); - gl.viewport(0, 0, dx, dy); - gl.scissor(0, 0, dx, dy); + state.viewport(0, 0, dx, dy); + state.scissor(0, 0, dx, dy); if (!texture) texture = colorBufferHalfFloat && textureHalfFloat ? resources.texture('volume-float16', 'rgba', 'fp16', 'linear') diff --git a/src/mol-math/geometry/primitives/box3d.ts b/src/mol-math/geometry/primitives/box3d.ts index d701ae18085f5b45feb9f29cb7cdd5682e9104f9..fff4923615107fddbbe9be77febc27b070182889 100644 --- a/src/mol-math/geometry/primitives/box3d.ts +++ b/src/mol-math/geometry/primitives/box3d.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2018-2020 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 David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -124,11 +124,19 @@ namespace Box3D { } export function containsVec3(box: Box3D, v: Vec3) { - return ( + return !( v[0] < box.min[0] || v[0] > box.max[0] || v[1] < box.min[1] || v[1] > box.max[1] || v[2] < box.min[2] || v[2] > box.max[2] - ) ? false : true; + ); + } + + export function overlaps(a: Box3D, b: Box3D) { + return !( + a.max[0] < b.min[0] || a.min[0] > b.max[0] || + a.max[1] < b.min[1] || a.min[1] > b.max[1] || + a.max[2] < b.min[2] || a.min[2] > b.max[2] + ); } } diff --git a/src/mol-model-formats/structure/mol.ts b/src/mol-model-formats/structure/mol.ts index 942f24a597cd0c8bbb18c564d571536e9310f49f..f32849f738b1bb31f4a9aca641bbe2a55adf1e91 100644 --- a/src/mol-model-formats/structure/mol.ts +++ b/src/mol-model-formats/structure/mol.ts @@ -80,7 +80,10 @@ export async function getMolModels(mol: MolFile, format: ModelFormat<any> | unde const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdxA, x => x - 1, Int32Array)); const indexB = Column.ofIntArray(Column.mapToArray(bonds.atomIdxB, x => x - 1, Int32Array)); const order = Column.asArrayColumn(bonds.order, Int32Array); - const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: atoms.count }); + const pairBonds = IndexPairBonds.fromData( + { pairs: { indexA, indexB, order }, count: atoms.count }, + { maxDistance: Infinity } + ); IndexPairBonds.Provider.set(models.representative, pairBonds); } diff --git a/src/mol-model-formats/structure/mol2.ts b/src/mol-model-formats/structure/mol2.ts index ac8b4e75c1119a06afa11a91d18709f888129418..19723a6fd1987e77bb382bafb2621f98e95bfbeb 100644 --- a/src/mol-model-formats/structure/mol2.ts +++ b/src/mol-model-formats/structure/mol2.ts @@ -113,7 +113,10 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) { return BondType.Flag.Covalent; } }, Int8Array)); - const pairBonds = IndexPairBonds.fromData({ pairs: { key, indexA, indexB, order, flag }, count: atoms.count }); + const pairBonds = IndexPairBonds.fromData( + { pairs: { key, indexA, indexB, order, flag }, count: atoms.count }, + { maxDistance: crysin ? -1 : Infinity } + ); const first = _models.representative; IndexPairBonds.Provider.set(first, pairBonds); diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts index 8f60da9d85a008d47d0120c60fdce7ceff08b03b..47580e1985a8d146db25eb5c2111234d59c6799e 100644 --- a/src/mol-model-props/common/custom-element-property.ts +++ b/src/mol-model-props/common/custom-element-property.ts @@ -106,7 +106,7 @@ namespace CustomElementProperty { factory: Coloring, getParams: () => ({}), defaultValues: {}, - isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !!modelProperty.get(ctx.structure.models[0]).value, + isApplicable: (ctx: ThemeDataContext) => !!ctx.structure, ensureCustomProperties: { attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? modelProperty.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(), detach: (data: ThemeDataContext) => data.structure && data.structure.models[0].customProperties.reference(modelProperty.descriptor, false) diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts index 81f84f447fa5c17191641c2d8c3b81594bcdc08d..3c2dfdc6322d5895bf82dbd5c6689c78b58d4bae 100644 --- a/src/mol-model/structure/structure/structure.ts +++ b/src/mol-model/structure/structure/structure.ts @@ -23,7 +23,7 @@ import { Carbohydrates } from './carbohydrates/data'; import { computeCarbohydrates } from './carbohydrates/compute'; import { Vec3, Mat4 } from '../../../mol-math/linear-algebra'; import { idFactory } from '../../../mol-util/id-factory'; -import { GridLookup3D } from '../../../mol-math/geometry'; +import { Box3D, GridLookup3D } from '../../../mol-math/geometry'; import { UUID } from '../../../mol-util'; import { CustomProperties } from '../../custom-property'; import { AtomicHierarchy } from '../model/properties/atomic'; @@ -43,6 +43,8 @@ type State = { lookup3d?: StructureLookup3D, interUnitBonds?: InterUnitBonds, dynamicBonds: boolean, + interBondsValidUnit?: (unit: Unit) => boolean, + interBondsValidUnitPair?: (structure: Structure, unitA: Unit, unitB: Unit) => boolean, unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>, unitSymmetryGroupsIndexMap?: IntMap<number>, unitsSortedByVolume?: ReadonlyArray<Unit>; @@ -241,6 +243,8 @@ class Structure { this.state.interUnitBonds = computeInterUnitBonds(this, { ignoreWater: !this.dynamicBonds, ignoreIon: !this.dynamicBonds, + validUnit: this.state.interBondsValidUnit, + validUnitPair: this.state.interBondsValidUnitPair, }); } return this.state.interUnitBonds; @@ -250,6 +254,14 @@ class Structure { return this.state.dynamicBonds; } + get interBondsValidUnit() { + return this.state.interBondsValidUnit; + } + + get interBondsValidUnitPair() { + return this.state.interBondsValidUnitPair; + } + get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> { if (this.state.unitSymmetryGroups) return this.state.unitSymmetryGroups; this.state.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this); @@ -380,7 +392,12 @@ class Structure { parent: parent?.remapModel(m), label: this.label, interUnitBonds: dynamicBonds ? undefined : interUnitBonds, - dynamicBonds + dynamicBonds, + interBondsValidUnit: this.state.interBondsValidUnit, + interBondsValidUnitPair: this.state.interBondsValidUnitPair, + coordinateSystem: this.state.coordinateSystem, + masterModel: this.state.masterModel, + representativeModel: this.state.representativeModel, }); } @@ -428,7 +445,6 @@ class Structure { function cmpUnits(units: ArrayLike<Unit>, i: number, j: number) { return units[i].id - units[j].id; - } function getModels(s: Structure) { @@ -634,6 +650,8 @@ namespace Structure { * Also enables calculation of inter-unit bonds in water molecules. */ dynamicBonds?: boolean, + interBondsValidUnit?: (unit: Unit) => boolean, + interBondsValidUnitPair?: (structure: Structure, unitA: Unit, unitB: Unit) => boolean, coordinateSystem?: SymmetryOperator label?: string /** Master model for structures of a protein model and multiple ligand models */ @@ -722,6 +740,12 @@ namespace Structure { if (props.parent) state.parent = props.parent.parent || props.parent; if (props.interUnitBonds) state.interUnitBonds = props.interUnitBonds; + if (props.interBondsValidUnit) state.interBondsValidUnit = props.interBondsValidUnit; + else if (props.parent) state.interBondsValidUnit = props.parent.interBondsValidUnit; + + if (props.interBondsValidUnitPair) state.interBondsValidUnitPair = props.interBondsValidUnitPair; + else if (props.parent) state.interBondsValidUnitPair = props.parent.interBondsValidUnitPair; + if (props.dynamicBonds) state.dynamicBonds = props.dynamicBonds; else if (props.parent) state.dynamicBonds = props.parent.dynamicBonds; @@ -1180,7 +1204,7 @@ namespace Structure { /** * Iterate over all unit pairs of a structure and invokes callback for valid units - * and unit pairs if within a max distance. + * and unit pairs if their boundaries are within a max distance. */ export function eachUnitPair(structure: Structure, callback: (unitA: Unit, unitB: Unit) => void, props: EachUnitPairProps) { const { maxRadius, validUnit, validUnitPair } = props; @@ -1188,15 +1212,19 @@ namespace Structure { const lookup = structure.lookup3d; const imageCenter = Vec3(); + const bbox = Box3D(); + const rvec = Vec3.create(maxRadius, maxRadius, maxRadius); for (const unit of structure.units) { if (!validUnit(unit)) continue; const bs = unit.boundary.sphere; + Box3D.expand(bbox, unit.boundary.box, rvec); Vec3.transformMat4(imageCenter, bs.center, unit.conformation.operator.matrix); const closeUnits = lookup.findUnitIndices(imageCenter[0], imageCenter[1], imageCenter[2], bs.radius + maxRadius); for (let i = 0; i < closeUnits.count; i++) { const other = structure.units[closeUnits.indices[i]]; + if (!Box3D.overlaps(bbox, other.boundary.box)) continue; if (!validUnit(other) || unit.id >= other.id || !validUnitPair(unit, other)) continue; if (other.elements.length >= unit.elements.length) callback(unit, other); diff --git a/src/mol-model/structure/structure/unit/bonds/inter-compute.ts b/src/mol-model/structure/structure/unit/bonds/inter-compute.ts index 535a6d09bca596e00119faffd2cf2fcefafd9344..ba0327c3fc2ffaecbc25b4abe2ed8b434a40373d 100644 --- a/src/mol-model/structure/structure/unit/bonds/inter-compute.ts +++ b/src/mol-model/structure/structure/unit/bonds/inter-compute.ts @@ -21,12 +21,18 @@ import { StructConn } from '../../../../../mol-model-formats/structure/property/ import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common'; import { Model } from '../../../model'; +// avoiding namespace lookup improved performance in Chrome (Aug 2020) +const v3distance = Vec3.distance; +const v3set = Vec3.set; +const v3squaredDistance = Vec3.squaredDistance; +const v3transformMat4 = Vec3.transformMat4; + const tmpDistVecA = Vec3(); const tmpDistVecB = Vec3(); function getDistance(unitA: Unit.Atomic, indexA: ElementIndex, unitB: Unit.Atomic, indexB: ElementIndex) { unitA.conformation.position(indexA, tmpDistVecA); unitB.conformation.position(indexB, tmpDistVecB); - return Vec3.distance(tmpDistVecA, tmpDistVecB); + return v3distance(tmpDistVecA, tmpDistVecB); } const _imageTransform = Mat4(); @@ -68,22 +74,22 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) { const aI = atomsA[_aI]; - Vec3.set(_imageA, xA[aI], yA[aI], zA[aI]); - if (isNotIdentity) Vec3.transformMat4(_imageA, _imageA, imageTransform); - if (Vec3.squaredDistance(_imageA, bCenter) > testDistanceSq) continue; + v3set(_imageA, xA[aI], yA[aI], zA[aI]); + if (isNotIdentity) v3transformMat4(_imageA, _imageA, imageTransform); + if (v3squaredDistance(_imageA, bCenter) > testDistanceSq) continue; if (!props.forceCompute && indexPairs) { const { maxDistance } = indexPairs; const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds; const srcA = sourceIndex.value(aI); + const aeI = getElementIdx(type_symbolA.value(aI)); for (let i = offset[srcA], il = offset[srcA + 1]; i < il; ++i) { const bI = invertedIndex![b[i]]; const _bI = SortedArray.indexOf(unitB.elements, bI) as StructureElement.UnitIndex; if (_bI < 0) continue; - const aeI = getElementIdx(type_symbolA.value(aI)); const beI = getElementIdx(type_symbolA.value(bI)); const d = distance[i]; @@ -191,6 +197,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput } export interface InterBondComputationProps extends BondComputationProps { + validUnit: (unit: Unit) => boolean validUnitPair: (structure: Structure, unitA: Unit, unitB: Unit) => boolean ignoreWater: boolean ignoreIon: boolean @@ -215,7 +222,7 @@ function findBonds(structure: Structure, props: InterBondComputationProps) { findPairBonds(unitA as Unit.Atomic, unitB as Unit.Atomic, props, builder); }, { maxRadius: props.maxRadius, - validUnit: (unit: Unit) => Unit.isAtomic(unit), + validUnit: (unit: Unit) => props.validUnit(unit), validUnitPair: (unitA: Unit, unitB: Unit) => props.validUnitPair(structure, unitA, unitB) }); @@ -226,6 +233,7 @@ function computeInterUnitBonds(structure: Structure, props?: Partial<InterBondCo const p = { ...DefaultInterBondComputationProps, ...props }; return findBonds(structure, { ...p, + validUnit: (props && props.validUnit) || (u => Unit.isAtomic(u)), validUnitPair: (props && props.validUnitPair) || ((s, a, b) => { const mtA = a.model.atomicHierarchy.derived.residue.moleculeType; const mtB = b.model.atomicHierarchy.derived.residue.moleculeType; diff --git a/src/mol-model/structure/structure/unit/bonds/intra-compute.ts b/src/mol-model/structure/structure/unit/bonds/intra-compute.ts index 105347895b27c9080d7e5daeca079c975cbb6261..a4be859321bf19f44ea5c7902fff9d373e7565d8 100644 --- a/src/mol-model/structure/structure/unit/bonds/intra-compute.ts +++ b/src/mol-model/structure/structure/unit/bonds/intra-compute.ts @@ -21,6 +21,9 @@ import { ElementIndex } from '../../../model/indexing'; import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common'; import { Model } from '../../../model/model'; +// avoiding namespace lookup improved performance in Chrome (Aug 2020) +const v3distance = Vec3.distance; + function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], atomCount: number, canRemap: boolean): IntraUnitBonds { const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB); const flags = new Uint16Array(builder.slotCount); @@ -39,7 +42,7 @@ const tmpDistVecB = Vec3(); function getDistance(unit: Unit.Atomic, indexA: ElementIndex, indexB: ElementIndex) { unit.conformation.position(indexA, tmpDistVecA); unit.conformation.position(indexB, tmpDistVecB); - return Vec3.distance(tmpDistVecA, tmpDistVecB); + return v3distance(tmpDistVecA, tmpDistVecB); } const __structConnAdded = new Set<StructureElement.UnitIndex>(); diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx index 73f819331fbc1188e0d594da430e1fb3ec6af0be..122e11f8a64fa9d987db949223698ec55fc66771 100644 --- a/src/mol-plugin-ui/viewport/simple-settings.tsx +++ b/src/mol-plugin-ui/viewport/simple-settings.tsx @@ -8,8 +8,10 @@ import { produce } from 'immer'; import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d'; import { PluginCommands } from '../../mol-plugin/commands'; +import { PluginConfig } from '../../mol-plugin/config'; import { StateTransform } from '../../mol-state'; import { Color } from '../../mol-util/color'; +import { deepClone } from '../../mol-util/object'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { ParamMapping } from '../../mol-util/param-mapping'; import { Mutable } from '../../mol-util/type-helpers'; @@ -50,7 +52,8 @@ const SimpleSettingsParams = { camera: Canvas3DParams.camera, background: PD.Group({ color: PD.Color(Color(0xFCFBF9), { label: 'Background', description: 'Custom background color' }), - transparent: PD.Boolean(false) + transparent: PD.Boolean(false), + style: Canvas3DParams.postprocessing.params.background, }, { pivot: 'color' }), lighting: PD.Group({ occlusion: Canvas3DParams.postprocessing.params.occlusion, @@ -75,6 +78,13 @@ const SimpleSettingsMapping = ParamMapping({ if (controls.left !== 'none') options.push(['left', LayoutOptions.left]); params.layout.options = options; } + const bgStyles = ctx.config.get(PluginConfig.Background.Styles) || []; + if (bgStyles.length > 0) { + Object.assign(params.background.params.style, { + presets: deepClone(bgStyles), + isFlat: false, // so the presets menu is shown + }); + } return params; }, target(ctx: PluginUIContext) { @@ -97,7 +107,8 @@ const SimpleSettingsMapping = ParamMapping({ camera: canvas.camera, background: { color: renderer.backgroundColor, - transparent: canvas.transparentBackground + transparent: canvas.transparentBackground, + style: canvas.postprocessing.background, }, lighting: { occlusion: canvas.postprocessing.occlusion, @@ -117,6 +128,7 @@ const SimpleSettingsMapping = ParamMapping({ canvas.renderer.backgroundColor = s.background.color; canvas.postprocessing.occlusion = s.lighting.occlusion; canvas.postprocessing.outline = s.lighting.outline; + canvas.postprocessing.background = s.background.style; canvas.cameraFog = s.lighting.fog; canvas.cameraClipping = { radius: s.clipping.radius, diff --git a/src/mol-plugin/config.ts b/src/mol-plugin/config.ts index b70305fb7f6773cc470aca8bc9733a15db101d87..2abb40578fafff36078c892e11fc9247f74cc9e0 100644 --- a/src/mol-plugin/config.ts +++ b/src/mol-plugin/config.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 David Sehnal <david.sehnal@gmail.com> * @author Alexander Rose <alexander.rose@weirdbyte.de> @@ -12,6 +12,7 @@ import { EmdbDownloadProvider } from '../mol-plugin-state/actions/volume'; import { StructureRepresentationPresetProvider } from '../mol-plugin-state/builder/structure/representation-preset'; import { PluginFeatureDetection } from './features'; import { SaccharideCompIdMapType } from '../mol-model/structure/structure/carbohydrates/constants'; +import { BackgroundProps } from '../mol-canvas3d/passes/background'; export class PluginConfigItem<T = any> { toString() { return this.key; } @@ -65,6 +66,9 @@ export const PluginConfig = { DefaultRepresentationPreset: item<string>('structure.default-representation-preset', 'auto'), DefaultRepresentationPresetParams: item<StructureRepresentationPresetProvider.CommonParams>('structure.default-representation-preset-params', { }), SaccharideCompIdMapType: item<SaccharideCompIdMapType>('structure.saccharide-comp-id-map-type', 'default'), + }, + Background: { + Styles: item<[BackgroundProps, string][]>('background.styles', []), } }; diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts index 29ec1c03d65a88bbff9c669fd86e3eed0a5901b4..405087080f0c118dece7b9c2ec541134ddcf23d0 100644 --- a/src/mol-plugin/context.ts +++ b/src/mol-plugin/context.ts @@ -201,7 +201,7 @@ export class PluginContext { const pickPadding = this.config.get(PluginConfig.General.PickPadding) ?? 1; const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false; const preferWebGl1 = this.config.get(PluginConfig.General.PreferWebGl1) || false; - (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, preferWebGl1 }); + (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, this.managers.asset, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, preferWebGl1 }); } (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!); this.canvas3dInit.next(true); diff --git a/src/mol-plugin/util/viewport-screenshot.ts b/src/mol-plugin/util/viewport-screenshot.ts index 261ae3708d95a5c428b162b35ff69242d306c30c..97368fde642536abe397a736829216e82cbee236 100644 --- a/src/mol-plugin/util/viewport-screenshot.ts +++ b/src/mol-plugin/util/viewport-screenshot.ts @@ -309,7 +309,9 @@ class ViewportScreenshotHelper extends PluginComponent { if (width <= 0 || height <= 0) return; await ctx.update('Rendering image...'); - const imageData = this.imagePass.getImageData(width, height, viewport); + const pass = this.imagePass; + await pass.updateBackground(); + const imageData = pass.getImageData(width, height, viewport); await ctx.update('Encoding image...'); const canvas = this.canvas; diff --git a/src/tests/browser/marching-cubes.ts b/src/tests/browser/marching-cubes.ts index 5f585b77984a400c5b3ceaa0979db8dbe1ec8a52..b9ef65cb60e415ff238cd5fe8724cebc13df73b5 100644 --- a/src/tests/browser/marching-cubes.ts +++ b/src/tests/browser/marching-cubes.ts @@ -22,6 +22,7 @@ import { Representation } from '../../mol-repr/representation'; import { computeMarchingCubesMesh } from '../../mol-geo/util/marching-cubes/algorithm'; import { Mesh } from '../../mol-geo/geometry/mesh/mesh'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -31,7 +32,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), { +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), { renderer: { backgroundColor: ColorNames.white }, camera: { mode: 'orthographic' } })); diff --git a/src/tests/browser/render-lines.ts b/src/tests/browser/render-lines.ts index d4996f777d1ffcb2865e951c41935c296d7aa03d..137ca2a69f9d71023fd9d3f5d4ba3af469b6bcf6 100644 --- a/src/tests/browser/render-lines.ts +++ b/src/tests/browser/render-lines.ts @@ -15,6 +15,7 @@ import { Color } from '../../mol-util/color'; import { createRenderObject } from '../../mol-gl/render-object'; import { Representation } from '../../mol-repr/representation'; import { ParamDefinition } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -24,7 +25,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function linesRepr() { diff --git a/src/tests/browser/render-mesh.ts b/src/tests/browser/render-mesh.ts index 12861bad246a92f054407259eef86686d45cfbde..e639b931e7dc826a5f7ab86808ed74cc8da1c8ce 100644 --- a/src/tests/browser/render-mesh.ts +++ b/src/tests/browser/render-mesh.ts @@ -17,6 +17,7 @@ import { createRenderObject } from '../../mol-gl/render-object'; import { Representation } from '../../mol-repr/representation'; import { Torus } from '../../mol-geo/primitive/torus'; import { ParamDefinition } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -26,7 +27,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function meshRepr() { diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts index cf83c1c9a4f8518ecf84416e804b07e594ee5552..184d273939372d42893fa6086f06b32792266cd0 100644 --- a/src/tests/browser/render-shape.ts +++ b/src/tests/browser/render-shape.ts @@ -19,6 +19,7 @@ import { Sphere } from '../../mol-geo/primitive/sphere'; import { ColorNames } from '../../mol-util/color/names'; import { Shape } from '../../mol-model/shape'; import { ShapeRepresentation } from '../../mol-repr/shape/representation'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -28,6 +29,8 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); +const assetManager = new AssetManager(); + const info = document.createElement('div'); info.style.position = 'absolute'; info.style.fontFamily = 'sans-serif'; @@ -38,7 +41,7 @@ info.style.color = 'white'; parent.appendChild(info); let prevReprLoci = Representation.Loci.Empty; -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); canvas3d.input.move.subscribe(({ x, y }) => { const pickingId = canvas3d.identify(x, y)?.id; diff --git a/src/tests/browser/render-spheres.ts b/src/tests/browser/render-spheres.ts index 439429fe35d6efec99e0ec225ab266b48e98b8a0..ed4e92ae278411bd11b155dbb3cc23fd38fd6b67 100644 --- a/src/tests/browser/render-spheres.ts +++ b/src/tests/browser/render-spheres.ts @@ -13,6 +13,7 @@ import { Color } from '../../mol-util/color'; import { createRenderObject } from '../../mol-gl/render-object'; import { Representation } from '../../mol-repr/representation'; import { ParamDefinition } from '../../mol-util/param-definition'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -22,7 +23,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function spheresRepr() { diff --git a/src/tests/browser/render-structure.ts b/src/tests/browser/render-structure.ts index 07e23ee4918d7a8698ceb01cc843ec62a4ceb614..634ccd8adeb7366d499ef77d0afffc6444961317 100644 --- a/src/tests/browser/render-structure.ts +++ b/src/tests/browser/render-structure.ts @@ -37,7 +37,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); const info = document.createElement('div'); @@ -123,7 +125,7 @@ function getMembraneOrientationRepr() { } async function init() { - const ctx = { runtime: SyncRuntimeContext, assetManager: new AssetManager() }; + const ctx = { runtime: SyncRuntimeContext, assetManager }; const cif = await downloadFromPdb('3pqr'); const models = await getModels(cif); diff --git a/src/tests/browser/render-text.ts b/src/tests/browser/render-text.ts index c25a45fa195c9d1e3d8eaf1f0b32db0a0058bcbe..b1b0a33a09346f88270be7bf3138ed366b0e4ece 100644 --- a/src/tests/browser/render-text.ts +++ b/src/tests/browser/render-text.ts @@ -15,6 +15,7 @@ import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder'; import { createRenderObject } from '../../mol-gl/render-object'; import { Spheres } from '../../mol-geo/geometry/spheres/spheres'; import { resizeCanvas } from '../../mol-canvas3d/util'; +import { AssetManager } from '../../mol-util/assets'; const parent = document.getElementById('app')!; parent.style.width = '100%'; @@ -24,7 +25,9 @@ const canvas = document.createElement('canvas'); parent.appendChild(canvas); resizeCanvas(canvas, parent); -const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas)); +const assetManager = new AssetManager(); + +const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager)); canvas3d.animate(); function textRepr() { diff --git a/webpack.config.common.js b/webpack.config.common.js index 491eb8d551226831574049bcd850af99c1b939f7..380e1cc3e871daf21e71553e7b0efe452aacd3cc 100644 --- a/webpack.config.common.js +++ b/webpack.config.common.js @@ -30,7 +30,11 @@ const sharedConfig = { { loader: 'css-loader', options: { sourceMap: false } }, { loader: 'sass-loader', options: { sourceMap: false } }, ] - } + }, + { + test: /\.(jpg)$/i, + type: 'asset/resource', + }, ] }, plugins: [ @@ -76,7 +80,7 @@ function createEntry(src, outFolder, outFilename, isNode) { function createEntryPoint(name, dir, out, library) { return { entry: path.resolve(__dirname, `lib/${dir}/${name}.js`), - output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd' }, + output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd', assetModuleFilename: 'images/[hash][ext][query]', 'publicPath': '' }, ...sharedConfig }; }