123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- * @author David Sehnal <david.sehnal@gmail.com>
- */
- import { BehaviorSubject, Subscription } from 'rxjs';
- import { now } from '../mol-util/now';
- import { Vec3 } from '../mol-math/linear-algebra'
- import InputObserver, { ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer'
- 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 { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
- import { Representation } from '../mol-repr/representation';
- import Scene from '../mol-gl/scene';
- import { GraphicsRenderVariant } from '../mol-gl/webgl/render-item';
- import { PickingId } from '../mol-geo/geometry/picking';
- import { MarkerAction } from '../mol-util/marker-action';
- import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
- import { Camera } from './camera';
- import { ParamDefinition as PD } from '../mol-util/param-definition';
- import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
- import { SetUtils } from '../mol-util/set';
- import { Canvas3dInteractionHelper } from './helper/interaction-events';
- import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
- import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
- import { PixelData } from '../mol-util/image';
- import { readTexture } from '../mol-gl/compute/util';
- import { DrawPass } from './passes/draw';
- import { PickPass } from './passes/pick';
- import { Task } from '../mol-task';
- import { ImagePass, ImageProps } from './passes/image';
- import { Sphere3D } from '../mol-math/geometry';
- import { isDebugMode } from '../mol-util/debug';
- export const Canvas3DParams = {
- cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
- cameraFog: PD.Numeric(50, { min: 0, max: 100, step: 1 }),
- cameraClipFar: PD.Boolean(true),
- cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
- transparentBackground: PD.Boolean(false),
- multiSample: PD.Group(MultiSampleParams),
- postprocessing: PD.Group(PostprocessingParams),
- renderer: PD.Group(RendererParams),
- trackball: PD.Group(TrackballControlsParams),
- debug: PD.Group(DebugHelperParams)
- }
- export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
- export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
- export { Canvas3D }
- interface Canvas3D {
- readonly webgl: WebGLContext,
- add(repr: Representation.Any): void
- remove(repr: Representation.Any): void
- /**
- * This function must be called if animate() is not set up so that add/remove actions take place.
- */
- commit(isSynchronous?: boolean): void
- update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
- clear(): void
- requestDraw(force?: boolean): void
- animate(): void
- identify(x: number, y: number): PickingId | undefined
- mark(loci: Representation.Loci, action: MarkerAction): void
- getLoci(pickingId: PickingId): Representation.Loci
- readonly didDraw: BehaviorSubject<now.Timestamp>
- readonly reprCount: BehaviorSubject<number>
- handleResize(): void
- /** Focuses camera on scene's bounding sphere, centered and zoomed. */
- requestCameraReset(durationMs?: number): void
- readonly camera: Camera
- readonly boundingSphere: Readonly<Sphere3D>
- downloadScreenshot(): void
- getPixelData(variant: GraphicsRenderVariant): PixelData
- setProps(props: Partial<Canvas3DProps>): void
- getImagePass(): ImagePass
- /** Returns a copy of the current Canvas3D instance props */
- readonly props: Readonly<Canvas3DProps>
- readonly input: InputObserver
- readonly stats: RendererStats
- readonly interaction: Canvas3dInteractionHelper['events']
- dispose(): void
- }
- const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()))
- const DefaultRunTask = (task: Task<unknown>) => task.run()
- namespace Canvas3D {
- export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
- export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
- export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) {
- const gl = getGLContext(canvas, {
- alpha: true,
- antialias: true,
- depth: true,
- preserveDrawingBuffer: true,
- premultipliedAlpha: false,
- })
- if (gl === null) throw new Error('Could not create a WebGL rendering context')
- const input = InputObserver.fromElement(canvas)
- const webgl = createContext(gl)
- if (isDebugMode) {
- const loseContextExt = gl.getExtension('WEBGL_lose_context')
- if (loseContextExt) {
- canvas.addEventListener('mousedown', e => {
- if (webgl.isContextLost) return
- if (!e.shiftKey || !e.ctrlKey || !e.altKey) return
- console.log('lose context')
- loseContextExt.loseContext()
- setTimeout(() => {
- if (!webgl.isContextLost) return
- console.log('restore context')
- loseContextExt.restoreContext()
- }, 1000)
- }, false)
- }
- }
- // https://www.khronos.org/webgl/wiki/HandlingContextLost
- canvas.addEventListener('webglcontextlost', e => {
- webgl.setContextLost()
- e.preventDefault()
- if (isDebugMode) console.log('context lost')
- }, false)
- canvas.addEventListener('webglcontextrestored', () => {
- if (!webgl.isContextLost) return
- webgl.handleContextRestored()
- if (isDebugMode) console.log('context restored')
- }, false)
- return Canvas3D.create(webgl, input, props, runTask)
- }
- export function create(webgl: WebGLContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D {
- const p = { ...DefaultCanvas3DParams, ...props }
- const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
- const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
- const reprCount = new BehaviorSubject(0)
- const startTime = now()
- const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
- const { gl, contextRestored } = webgl
- let width = gl.drawingBufferWidth
- let height = gl.drawingBufferHeight
- const scene = Scene.create(webgl)
- const camera = new Camera({
- position: Vec3.create(0, 0, 100),
- mode: p.cameraMode,
- fog: p.cameraFog,
- clipFar: p.cameraClipFar
- })
- const controls = TrackballControls.create(input, camera, p.trackball)
- const renderer = Renderer.create(webgl, p.renderer)
- const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
- const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
- const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper)
- const pickPass = new PickPass(webgl, renderer, scene, camera, 0.5)
- const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
- const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
- const contextRestoredSub = contextRestored.subscribe(() => {
- pickPass.pickDirty = true
- draw(true)
- })
- let drawPending = false
- let cameraResetRequested = false
- let nextCameraResetDuration: number | undefined = void 0
- function getLoci(pickingId: PickingId) {
- let loci: Loci = EmptyLoci
- let repr: Representation.Any = Representation.Empty
- reprRenderObjects.forEach((_, _repr) => {
- const _loci = _repr.getLoci(pickingId)
- if (!isEmptyLoci(_loci)) {
- if (!isEmptyLoci(loci)) {
- console.warn('found another loci, this should not happen')
- }
- loci = _loci
- repr = _repr
- }
- })
- return { loci, repr }
- }
- function mark(reprLoci: Representation.Loci, action: MarkerAction) {
- const { repr, loci } = reprLoci
- let changed = false
- if (repr) {
- changed = repr.mark(loci, action)
- } else {
- reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
- }
- if (changed) {
- scene.update(void 0, true)
- const prevPickDirty = pickPass.pickDirty
- draw(true)
- pickPass.pickDirty = prevPickDirty // marking does not change picking buffers
- }
- }
- function render(force: boolean) {
- if (webgl.isContextLost) return false
- let didRender = false
- controls.update(currentTime)
- Viewport.set(camera.viewport, 0, 0, width, height)
- const cameraChanged = camera.update()
- multiSample.update(force || cameraChanged, currentTime)
- if (force || cameraChanged || multiSample.enabled) {
- renderer.setViewport(0, 0, width, height)
- if (multiSample.enabled) {
- multiSample.render(true, p.transparentBackground)
- } else {
- drawPass.render(!postprocessing.enabled, p.transparentBackground)
- if (postprocessing.enabled) postprocessing.render(true)
- }
- pickPass.pickDirty = true
- didRender = true
- }
- return didRender;
- }
- let forceNextDraw = false;
- let currentTime = 0;
- function draw(force?: boolean) {
- if (render(!!force || forceNextDraw)) {
- didDraw.next(now() - startTime as now.Timestamp)
- }
- forceNextDraw = false;
- drawPending = false
- }
- function requestDraw(force?: boolean) {
- if (drawPending) return
- drawPending = true
- forceNextDraw = !!force;
- }
- function animate() {
- currentTime = now();
- commit();
- camera.transition.tick(currentTime);
- draw(false);
- if (!camera.transition.inTransition && !webgl.isContextLost) {
- interactionHelper.tick(currentTime);
- }
- requestAnimationFrame(animate)
- }
- function identify(x: number, y: number): PickingId | undefined {
- return webgl.isContextLost ? undefined : pickPass.identify(x, y)
- }
- function commit(isSynchronous: boolean = false) {
- const allCommited = commitScene(isSynchronous);
- // Only reset the camera after the full scene has been commited.
- if (allCommited) resolveCameraReset();
- }
- function resolveCameraReset() {
- if (!cameraResetRequested) return;
- const { center, radius } = scene.boundingSphere;
- const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration
- camera.focus(center, radius, radius, duration);
- nextCameraResetDuration = void 0;
- cameraResetRequested = false;
- }
- const sceneCommitTimeoutMs = 250;
- function commitScene(isSynchronous: boolean) {
- if (!scene.needsCommit) return true;
- if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
- if (debugHelper.isEnabled) debugHelper.update();
- reprCount.next(reprRenderObjects.size);
- return true;
- }
- function add(repr: Representation.Any) {
- registerAutoUpdate(repr);
- const oldRO = reprRenderObjects.get(repr)
- const newRO = new Set<GraphicsRenderObject>()
- repr.renderObjects.forEach(o => newRO.add(o))
- if (oldRO) {
- if (!SetUtils.areEqual(newRO, oldRO)) {
- newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o) })
- oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o) })
- }
- } else {
- repr.renderObjects.forEach(o => scene.add(o))
- }
- reprRenderObjects.set(repr, newRO)
- scene.update(repr.renderObjects, false)
- }
- function remove(repr: Representation.Any) {
- unregisterAutoUpdate(repr);
- const renderObjects = reprRenderObjects.get(repr)
- if (renderObjects) {
- renderObjects.forEach(o => scene.remove(o))
- reprRenderObjects.delete(repr)
- scene.update(repr.renderObjects, false, true)
- }
- }
- function registerAutoUpdate(repr: Representation.Any) {
- if (reprUpdatedSubscriptions.has(repr)) return;
- reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
- if (!repr.state.syncManually) add(repr);
- }))
- }
- function unregisterAutoUpdate(repr: Representation.Any) {
- const updatedSubscription = reprUpdatedSubscriptions.get(repr);
- if (updatedSubscription) {
- updatedSubscription.unsubscribe();
- reprUpdatedSubscriptions.delete(repr);
- }
- }
- handleResize()
- return {
- webgl,
- add,
- remove,
- commit,
- update: (repr, keepSphere) => {
- if (repr) {
- if (!reprRenderObjects.has(repr)) return;
- scene.update(repr.renderObjects, !!keepSphere);
- } else {
- scene.update(void 0, !!keepSphere)
- }
- },
- clear: () => {
- reprUpdatedSubscriptions.forEach(v => v.unsubscribe())
- reprUpdatedSubscriptions.clear()
- reprRenderObjects.clear()
- scene.clear()
- debugHelper.clear()
- requestDraw(true)
- reprCount.next(reprRenderObjects.size)
- },
- // draw,
- requestDraw,
- animate,
- identify,
- mark,
- getLoci,
- handleResize,
- requestCameraReset: (durationMs) => {
- nextCameraResetDuration = durationMs;
- cameraResetRequested = true;
- },
- camera,
- boundingSphere: scene.boundingSphere,
- downloadScreenshot: () => {
- // TODO
- },
- getPixelData: (variant: GraphicsRenderVariant) => {
- switch (variant) {
- case 'color': return webgl.getDrawingBufferPixelData()
- case 'pickObject': return pickPass.objectPickTarget.getPixelData()
- case 'pickInstance': return pickPass.instancePickTarget.getPixelData()
- case 'pickGroup': return pickPass.groupPickTarget.getPixelData()
- case 'depth': return readTexture(webgl, drawPass.depthTexture) as PixelData
- }
- },
- didDraw,
- reprCount,
- setProps: (props: Partial<Canvas3DProps>) => {
- if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
- camera.setState({ mode: props.cameraMode })
- }
- if (props.cameraFog !== undefined && props.cameraFog !== camera.state.fog) {
- camera.setState({ fog: props.cameraFog })
- }
- if (props.cameraClipFar !== undefined && props.cameraClipFar !== camera.state.clipFar) {
- camera.setState({ clipFar: props.cameraClipFar })
- }
- if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs
- if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground
- if (props.postprocessing) postprocessing.setProps(props.postprocessing)
- if (props.multiSample) multiSample.setProps(props.multiSample)
- if (props.renderer) renderer.setProps(props.renderer)
- if (props.trackball) controls.setProps(props.trackball)
- if (props.debug) debugHelper.setProps(props.debug)
- requestDraw(true)
- },
- getImagePass: (props: Partial<ImageProps> = {}) => {
- return new ImagePass(webgl, renderer, scene, camera, debugHelper, props)
- },
- get props() {
- return {
- cameraMode: camera.state.mode,
- cameraFog: camera.state.fog,
- cameraClipFar: camera.state.clipFar,
- cameraResetDurationMs: p.cameraResetDurationMs,
- transparentBackground: p.transparentBackground,
- postprocessing: { ...postprocessing.props },
- multiSample: { ...multiSample.props },
- renderer: { ...renderer.props },
- trackball: { ...controls.props },
- debug: { ...debugHelper.props }
- }
- },
- get input() {
- return input
- },
- get stats() {
- return renderer.stats
- },
- get interaction() {
- return interactionHelper.events
- },
- dispose: () => {
- contextRestoredSub.unsubscribe()
- scene.clear()
- debugHelper.clear()
- input.dispose()
- controls.dispose()
- renderer.dispose()
- interactionHelper.dispose()
- }
- }
- function handleResize() {
- width = gl.drawingBufferWidth
- height = gl.drawingBufferHeight
- renderer.setViewport(0, 0, width, height)
- Viewport.set(camera.viewport, 0, 0, width, height)
- Viewport.set(controls.viewport, 0, 0, width, height)
- drawPass.setSize(width, height)
- pickPass.setSize(width, height)
- postprocessing.setSize(width, height)
- multiSample.setSize(width, height)
- requestDraw(true)
- }
- }
- }
|