diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index d241d4cbe250fe8facd1ea7512efb9d6507b3d7e..7076a1a899a6aeb07201ad736b07a37fed8ea1b9 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -6,10 +6,12 @@ */ import { Mat4, Vec3, Vec4, EPSILON } from '../mol-math/linear-algebra' -import { Viewport, cameraProject, cameraUnproject } from './camera/util'; +import { Viewport, cameraProject, cameraUnproject, cameraSetClipping } from './camera/util'; import { Object3D } from '../mol-gl/object3d'; import { BehaviorSubject } from 'rxjs'; import { CameraTransitionManager } from './camera/transition'; +import { Canvas3DProps } from './canvas3d'; +import Scene from '../mol-gl/scene'; export { Camera } @@ -80,8 +82,8 @@ class Camera implements Object3D { return changed; } - setState(snapshot: Partial<Camera.Snapshot>) { - this.transition.apply(snapshot); + setState(snapshot: Partial<Camera.Snapshot>, durationMs?: number) { + this.transition.apply(snapshot, durationMs); } getSnapshot() { @@ -90,30 +92,27 @@ class Camera implements Object3D { return ret; } - getFocus(target: Vec3, radius: number, dir?: Vec3): Partial<Camera.Snapshot> { + getFocus(target: Vec3, radius: number): Partial<Camera.Snapshot> { const fov = this.state.fov const { width, height } = this.viewport const aspect = width / height const aspectFactor = (height < width ? 1 : aspect) - const currentDistance = Vec3.distance(this.state.position, target) const targetDistance = Math.abs((radius / aspectFactor) / Math.sin(fov / 2)) - const deltaDistance = Math.abs(currentDistance - targetDistance) + Vec3.setMagnitude(this.deltaDirection, this.state.direction, targetDistance) + Vec3.sub(this.newPosition, target, this.deltaDirection) - if (dir) { - Vec3.setMagnitude(this.deltaDirection, dir, targetDistance) - Vec3.add(this.newPosition, target, this.deltaDirection) - } else { - Vec3.setMagnitude(this.deltaDirection, this.state.direction, deltaDistance) - if (currentDistance < targetDistance) Vec3.negate(this.deltaDirection, this.deltaDirection) - Vec3.add(this.newPosition, this.state.position, this.deltaDirection) - } + const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state); + state.target = Vec3.clone(target); + state.position = Vec3.clone(this.newPosition); + + cameraSetClipping(state, this.scene.boundingSphere, this.canvasProps) - return { target, position: Vec3.clone(this.newPosition) }; + return state; } - focus(target: Vec3, radius: number, dir?: Vec3) { - if (radius > 0) this.setState(this.getFocus(target, radius, dir)); + focus(target: Vec3, radius: number, durationMs?: number) { + if (radius > 0) this.setState(this.getFocus(target, radius), durationMs); } // lookAt(target: Vec3) { @@ -137,7 +136,7 @@ class Camera implements Object3D { this.updatedViewProjection.complete(); } - constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(-1, -1, 1, 1)) { + constructor(private scene: Scene, private canvasProps: Canvas3DProps, state?: Partial<Camera.Snapshot>, viewport = Viewport.create(-1, -1, 1, 1)) { this.viewport = viewport; Camera.copySnapshot(this.state, state); } @@ -202,7 +201,7 @@ namespace Camera { mode: Mode, position: Vec3, - // Normalized camera direction + // Normalized camera direction, from Target to Position, for some reason? direction: Vec3, up: Vec3, target: Vec3, @@ -217,7 +216,7 @@ namespace Camera { } export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) { - if (!source) return; + if (!source) return out; if (typeof source.mode !== 'undefined') out.mode = source.mode; @@ -233,6 +232,8 @@ namespace Camera { if (typeof source.fov !== 'undefined') out.fov = source.fov; if (typeof source.zoom !== 'undefined') out.zoom = source.zoom; + + return out; } } diff --git a/src/mol-canvas3d/camera/transition.ts b/src/mol-canvas3d/camera/transition.ts index 8e9f31a04b7212cfffce06f93b1238ad8c3ef964..a4639735bef760b6017886b93c3a775fabcb6a02 100644 --- a/src/mol-canvas3d/camera/transition.ts +++ b/src/mol-canvas3d/camera/transition.ts @@ -22,7 +22,7 @@ class CameraTransitionManager { private current = Camera.createDefaultSnapshot(); apply(to: Partial<Camera.Snapshot>, durationMs: number = 0, transition?: CameraTransitionManager.TransitionFunc) { - if (durationMs <= 0 || to.mode !== this.camera.state.mode) { + if (durationMs <= 0 || (typeof to.mode !== 'undefined' && to.mode !== this.camera.state.mode)) { this.finish(to); return; } diff --git a/src/mol-canvas3d/camera/util.ts b/src/mol-canvas3d/camera/util.ts index fcf9ba8934a92ccdcd4aff220eb956ab03e333c2..1d072ca231a9765131762602b554621450266016 100644 --- a/src/mol-canvas3d/camera/util.ts +++ b/src/mol-canvas3d/camera/util.ts @@ -5,6 +5,9 @@ */ import { Mat4, Vec3, Vec4, EPSILON } from '../../mol-math/linear-algebra' +import { Camera } from '../camera' +import { Sphere3D } from '../../mol-math/geometry' +import { Canvas3DProps } from '../canvas3d' export { Viewport } @@ -80,6 +83,43 @@ export function cameraLookAt(position: Vec3, up: Vec3, direction: Vec3, target: } } +export function cameraSetClipping(state: Camera.Snapshot, boundingSphere: Sphere3D, p: Canvas3DProps) { + const cDist = Vec3.distance(state.position, state.target) + const bRadius = Math.max(10, boundingSphere.radius) + + const nearFactor = (50 - p.clip[0]) / 50 + const farFactor = -(50 - p.clip[1]) / 50 + let near = cDist - (bRadius * nearFactor) + let far = cDist + (bRadius * farFactor) + + const fogNearFactor = (50 - p.fog[0]) / 50 + const fogFarFactor = -(50 - p.fog[1]) / 50 + let fogNear = cDist - (bRadius * fogNearFactor) + let fogFar = cDist + (bRadius * fogFarFactor) + + if (state.mode === 'perspective') { + // set at least to 5 to avoid slow sphere impostor rendering + near = Math.max(5, p.cameraClipDistance, near) + far = Math.max(5, far) + fogNear = Math.max(5, fogNear) + fogFar = Math.max(5, fogFar) + } else if (state.mode === 'orthographic') { + if (p.cameraClipDistance > 0) { + near = Math.max(p.cameraClipDistance, near) + } + } + + state.near = near; + state.far = far; + state.fogNear = fogNear; + state.fogFar = fogFar; + + // if (near !== currentNear || far !== currentFar || fogNear !== currentFogNear || fogFar !== currentFogFar) { + // camera.setState({ near, far, fogNear, fogFar }) + // currentNear = near, currentFar = far, currentFogNear = fogNear, currentFogFar = fogFar + // } +} + const NEAR_RANGE = 0 const FAR_RANGE = 1 diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index e4281628243c7eb34fe101c001ea5f70ab8456df..5cdee6e78c2654eb078ba432b5e0ebbaf9d89ef1 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -11,7 +11,7 @@ import InputObserver, { ModifiersKeys, ButtonsType } from '../mol-util/input/inp import Renderer, { RendererStats, RendererParams } from '../mol-gl/renderer' import { GraphicsRenderObject } from '../mol-gl/render-object' import { TrackballControls, TrackballControlsParams } from './controls/trackball' -import { Viewport } from './camera/util' +import { Viewport, cameraSetClipping } from './camera/util' import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context'; import { Representation } from '../mol-repr/representation'; import Scene from '../mol-gl/scene'; @@ -38,6 +38,7 @@ export const Canvas3DParams = { // maxFps: PD.Numeric(30), cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]), cameraClipDistance: PD.Numeric(0, { min: 0.0, max: 50.0, step: 0.1 }, { description: 'The distance between camera and scene at which to clip regardless of near clipping plane.' }), + cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }), clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }), fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }), @@ -116,19 +117,20 @@ namespace Canvas3D { const startTime = now() const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp) - const camera = new Camera({ - near: 0.1, - far: 10000, - position: Vec3.create(0, 0, 100), - mode: p.cameraMode - }) - const webgl = createContext(gl) let width = gl.drawingBufferWidth let height = gl.drawingBufferHeight const scene = Scene.create(webgl) + + const camera = new Camera(scene, p, { + near: 0.1, + far: 10000, + position: Vec3.create(0, 0, 100), + mode: p.cameraMode + }) + const controls = TrackballControls.create(input, camera, p.trackball) const renderer = Renderer.create(webgl, camera, p.renderer) const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug); @@ -140,7 +142,7 @@ namespace Canvas3D { const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample) let drawPending = false - let cameraResetRequested: boolean | Vec3 = false + let cameraResetRequested = false function getLoci(pickingId: PickingId) { let loci: Loci = EmptyLoci @@ -172,37 +174,8 @@ namespace Canvas3D { } } - let currentNear = -1, currentFar = -1, currentFogNear = -1, currentFogFar = -1 function setClipping() { - const cDist = Vec3.distance(camera.state.position, camera.state.target) - const bRadius = Math.max(10, scene.boundingSphere.radius) - - const nearFactor = (50 - p.clip[0]) / 50 - const farFactor = -(50 - p.clip[1]) / 50 - let near = cDist - (bRadius * nearFactor) - let far = cDist + (bRadius * farFactor) - - const fogNearFactor = (50 - p.fog[0]) / 50 - const fogFarFactor = -(50 - p.fog[1]) / 50 - let fogNear = cDist - (bRadius * fogNearFactor) - let fogFar = cDist + (bRadius * fogFarFactor) - - if (camera.state.mode === 'perspective') { - // set at least to 5 to avoid slow sphere impostor rendering - near = Math.max(5, p.cameraClipDistance, near) - far = Math.max(5, far) - fogNear = Math.max(5, fogNear) - fogFar = Math.max(5, fogFar) - } else if (camera.state.mode === 'orthographic') { - if (p.cameraClipDistance > 0) { - near = Math.max(p.cameraClipDistance, near) - } - } - - if (near !== currentNear || far !== currentFar || fogNear !== currentFogNear || fogFar !== currentFogFar) { - camera.setState({ near, far, fogNear, fogFar }) - currentNear = near, currentFar = far, currentFogNear = fogNear, currentFogFar = fogFar - } + cameraSetClipping(camera.state, scene.boundingSphere, p); } function render(variant: 'pick' | 'draw', force: boolean) { @@ -271,8 +244,7 @@ namespace Canvas3D { runTask(scene.commit()).then(() => { if (cameraResetRequested && !scene.isCommiting) { - const dir = typeof cameraResetRequested === 'boolean' ? undefined : cameraResetRequested - camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius, dir) + camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius) cameraResetRequested = false } if (debugHelper.isEnabled) debugHelper.update() @@ -345,11 +317,11 @@ namespace Canvas3D { getLoci, handleResize, - resetCamera: (dir?: Vec3) => { + resetCamera: (/*dir?: Vec3*/) => { if (scene.isCommiting) { - cameraResetRequested = dir || true + cameraResetRequested = true } else { - camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius, dir) + camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius, p.cameraResetDurationMs) requestDraw(true); } }, @@ -373,6 +345,7 @@ namespace Canvas3D { camera.setState({ mode: props.cameraMode }) } if (props.cameraClipDistance !== undefined) p.cameraClipDistance = props.cameraClipDistance + if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]] if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]] @@ -388,6 +361,7 @@ namespace Canvas3D { return { cameraMode: camera.state.mode, cameraClipDistance: p.cameraClipDistance, + cameraResetDurationMs: p.cameraResetDurationMs, clip: p.clip, fog: p.fog, diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts index 59663f5e53b7935811da7eb5f671083720d87d9c..f218a5463f000b92123437f8c9cf9c8b1fb17075 100644 --- a/src/mol-gl/_spec/renderer.spec.ts +++ b/src/mol-gl/_spec/renderer.spec.ts @@ -24,10 +24,11 @@ import { Color } from '../../mol-util/color'; import { Sphere3D } from '../../mol-math/geometry'; import { createEmptyOverpaint } from '../../mol-geo/geometry/overpaint-data'; import { createEmptyTransparency } from '../../mol-geo/geometry/transparency-data'; +import { DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d'; function createRenderer(gl: WebGLRenderingContext) { const ctx = createContext(gl) - const camera = new Camera({ + const camera = new Camera(Scene.create(ctx), DefaultCanvas3DParams, { near: 0.01, far: 10000, position: Vec3.create(0, 0, 50) diff --git a/src/mol-plugin/behavior/dynamic/camera.ts b/src/mol-plugin/behavior/dynamic/camera.ts index 5c4e50a2b775511fc4b80edb314d23dca8710d1f..e2b70b3ad290ae5f27c64642d20dfbedb6d4b8a7 100644 --- a/src/mol-plugin/behavior/dynamic/camera.ts +++ b/src/mol-plugin/behavior/dynamic/camera.ts @@ -9,23 +9,24 @@ import { ParamDefinition } from '../../../mol-util/param-definition'; import { PluginBehavior } from '../behavior'; import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observer'; -export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number }>({ +export const FocusLociOnSelect = PluginBehavior.create<{ minRadius: number, extraRadius: number, durationMs?: number }>({ name: 'focus-loci-on-select', category: 'interaction', - ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number }> { + ctor: class extends PluginBehavior.Handler<{ minRadius: number, extraRadius: number, durationMs?: number }> { register(): void { this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, buttons, modifiers }) => { if (!this.ctx.canvas3d || buttons !== ButtonsType.Flag.Primary || !ModifiersKeys.areEqual(modifiers, ModifiersKeys.None)) return; const sphere = Loci.getBoundingSphere(current.loci); if (!sphere) return; - this.ctx.canvas3d.camera.focus(sphere.center, Math.max(sphere.radius + this.params.extraRadius, this.params.minRadius)); + this.ctx.canvas3d.camera.focus(sphere.center, Math.max(sphere.radius + this.params.extraRadius, this.params.minRadius), this.params.durationMs); }); } }, params: () => ({ minRadius: ParamDefinition.Numeric(8, { min: 1, max: 50, step: 1 }), - extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci.' }) + extraRadius: ParamDefinition.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci.' }), + durationMs: ParamDefinition.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'Camera transition duration.' }) }), display: { name: 'Focus Loci on Select' } }); \ No newline at end of file diff --git a/src/mol-plugin/index.ts b/src/mol-plugin/index.ts index 5dffcda53e795767023686c367c80c5d3ba919ba..72b2e78270e506c9c30fe8f89d3c725fdb774757 100644 --- a/src/mol-plugin/index.ts +++ b/src/mol-plugin/index.ts @@ -63,7 +63,7 @@ export const DefaultPluginSpec: PluginSpec = { PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci), PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci), PluginSpec.Behavior(PluginBehaviors.Representation.DefaultLociLabelProvider), - PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 8, extraRadius: 4 }), + PluginSpec.Behavior(PluginBehaviors.Camera.FocusLociOnSelect, { minRadius: 8, extraRadius: 4, durationMs: 250 }), // PluginSpec.Behavior(PluginBehaviors.Labels.SceneLabels), PluginSpec.Behavior(PluginBehaviors.CustomProps.MolstarSecondaryStructure, { autoAttach: true }), PluginSpec.Behavior(PluginBehaviors.CustomProps.PDBeStructureQualityReport, { autoAttach: true, showTooltip: true }),