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
};
}