canvas3d.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. /**
  2. * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. */
  7. import { BehaviorSubject, Subscription } from 'rxjs';
  8. import { now } from '../mol-util/now';
  9. import { Vec3 } from '../mol-math/linear-algebra'
  10. import InputObserver, { ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer'
  11. import Renderer, { RendererStats, RendererParams } from '../mol-gl/renderer'
  12. import { GraphicsRenderObject } from '../mol-gl/render-object'
  13. import { TrackballControls, TrackballControlsParams } from './controls/trackball'
  14. import { Viewport } from './camera/util'
  15. import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
  16. import { Representation } from '../mol-repr/representation';
  17. import Scene from '../mol-gl/scene';
  18. import { GraphicsRenderVariant } from '../mol-gl/webgl/render-item';
  19. import { PickingId } from '../mol-geo/geometry/picking';
  20. import { MarkerAction } from '../mol-util/marker-action';
  21. import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
  22. import { Camera } from './camera';
  23. import { ParamDefinition as PD } from '../mol-util/param-definition';
  24. import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
  25. import { SetUtils } from '../mol-util/set';
  26. import { Canvas3dInteractionHelper } from './helper/interaction-events';
  27. import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
  28. import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
  29. import { PixelData } from '../mol-util/image';
  30. import { readTexture } from '../mol-gl/compute/util';
  31. import { DrawPass } from './passes/draw';
  32. import { PickPass } from './passes/pick';
  33. import { ImagePass, ImageProps } from './passes/image';
  34. import { Sphere3D } from '../mol-math/geometry';
  35. import { isDebugMode } from '../mol-util/debug';
  36. import { CameraHelper, CameraHelperParams } from './helper/camera-helper';
  37. export const Canvas3DParams = {
  38. camera: PD.Group({
  39. mode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']] as const, { label: 'Camera' }),
  40. helper: PD.Group(CameraHelperParams, { isFlat: true })
  41. }, { pivot: 'mode' }),
  42. cameraFog: PD.MappedStatic('on', {
  43. on: PD.Group({
  44. intensity: PD.Numeric(50, { min: 1, max: 100, step: 1 }),
  45. }),
  46. off: PD.Group({})
  47. }, { cycle: true, description: 'Show fog in the distance' }),
  48. cameraClipping: PD.Group({
  49. radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
  50. far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
  51. }, { pivot: 'radius' }),
  52. cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
  53. transparentBackground: PD.Boolean(false),
  54. multiSample: PD.Group(MultiSampleParams),
  55. postprocessing: PD.Group(PostprocessingParams),
  56. renderer: PD.Group(RendererParams),
  57. trackball: PD.Group(TrackballControlsParams),
  58. debug: PD.Group(DebugHelperParams)
  59. }
  60. export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
  61. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  62. export { Canvas3D }
  63. interface Canvas3D {
  64. readonly webgl: WebGLContext,
  65. add(repr: Representation.Any): void
  66. remove(repr: Representation.Any): void
  67. /**
  68. * This function must be called if animate() is not set up so that add/remove actions take place.
  69. */
  70. commit(isSynchronous?: boolean): void
  71. update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
  72. clear(): void
  73. syncVisibility(): void
  74. requestDraw(force?: boolean): void
  75. animate(): void
  76. identify(x: number, y: number): PickingId | undefined
  77. mark(loci: Representation.Loci, action: MarkerAction): void
  78. getLoci(pickingId: PickingId): Representation.Loci
  79. readonly didDraw: BehaviorSubject<now.Timestamp>
  80. readonly reprCount: BehaviorSubject<number>
  81. handleResize(): void
  82. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  83. requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
  84. readonly camera: Camera
  85. readonly boundingSphere: Readonly<Sphere3D>
  86. downloadScreenshot(): void
  87. getPixelData(variant: GraphicsRenderVariant): PixelData
  88. setProps(props: Partial<Canvas3DProps>): void
  89. getImagePass(): ImagePass
  90. /** Returns a copy of the current Canvas3D instance props */
  91. readonly props: Readonly<Canvas3DProps>
  92. readonly input: InputObserver
  93. readonly stats: RendererStats
  94. readonly interaction: Canvas3dInteractionHelper['events']
  95. dispose(): void
  96. }
  97. const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()))
  98. namespace Canvas3D {
  99. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  100. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  101. export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}) {
  102. const gl = getGLContext(canvas, {
  103. alpha: true,
  104. antialias: true,
  105. depth: true,
  106. preserveDrawingBuffer: true,
  107. premultipliedAlpha: false,
  108. })
  109. if (gl === null) throw new Error('Could not create a WebGL rendering context')
  110. const input = InputObserver.fromElement(canvas)
  111. const webgl = createContext(gl)
  112. if (isDebugMode) {
  113. const loseContextExt = gl.getExtension('WEBGL_lose_context')
  114. if (loseContextExt) {
  115. canvas.addEventListener('mousedown', e => {
  116. if (webgl.isContextLost) return
  117. if (!e.shiftKey || !e.ctrlKey || !e.altKey) return
  118. console.log('lose context')
  119. loseContextExt.loseContext()
  120. setTimeout(() => {
  121. if (!webgl.isContextLost) return
  122. console.log('restore context')
  123. loseContextExt.restoreContext()
  124. }, 1000)
  125. }, false)
  126. }
  127. }
  128. // https://www.khronos.org/webgl/wiki/HandlingContextLost
  129. canvas.addEventListener('webglcontextlost', e => {
  130. webgl.setContextLost()
  131. e.preventDefault()
  132. if (isDebugMode) console.log('context lost')
  133. }, false)
  134. canvas.addEventListener('webglcontextrestored', () => {
  135. if (!webgl.isContextLost) return
  136. webgl.handleContextRestored()
  137. if (isDebugMode) console.log('context restored')
  138. }, false)
  139. return Canvas3D.create(webgl, input, props)
  140. }
  141. export function create(webgl: WebGLContext, input: InputObserver, props: Partial<Canvas3DProps> = {}): Canvas3D {
  142. const p = { ...DefaultCanvas3DParams, ...props }
  143. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
  144. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
  145. const reprCount = new BehaviorSubject(0)
  146. const startTime = now()
  147. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
  148. const { gl, contextRestored } = webgl
  149. let width = gl.drawingBufferWidth
  150. let height = gl.drawingBufferHeight
  151. const scene = Scene.create(webgl)
  152. const camera = new Camera({
  153. position: Vec3.create(0, 0, 100),
  154. mode: p.camera.mode,
  155. fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
  156. clipFar: p.cameraClipping.far
  157. })
  158. const controls = TrackballControls.create(input, camera, p.trackball)
  159. const renderer = Renderer.create(webgl, p.renderer)
  160. const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
  161. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
  162. const cameraHelper = new CameraHelper(webgl, p.camera.helper);
  163. const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper, cameraHelper)
  164. const pickPass = new PickPass(webgl, renderer, scene, camera, 0.5)
  165. const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
  166. const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
  167. const contextRestoredSub = contextRestored.subscribe(() => {
  168. pickPass.pickDirty = true
  169. draw(true)
  170. })
  171. let drawPending = false
  172. let cameraResetRequested = false
  173. let nextCameraResetDuration: number | undefined = void 0
  174. let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0
  175. function getLoci(pickingId: PickingId) {
  176. let loci: Loci = EmptyLoci
  177. let repr: Representation.Any = Representation.Empty
  178. reprRenderObjects.forEach((_, _repr) => {
  179. const _loci = _repr.getLoci(pickingId)
  180. if (!isEmptyLoci(_loci)) {
  181. if (!isEmptyLoci(loci)) {
  182. console.warn('found another loci, this should not happen')
  183. }
  184. loci = _loci
  185. repr = _repr
  186. }
  187. })
  188. return { loci, repr }
  189. }
  190. function mark(reprLoci: Representation.Loci, action: MarkerAction) {
  191. const { repr, loci } = reprLoci
  192. let changed = false
  193. if (repr) {
  194. changed = repr.mark(loci, action)
  195. } else {
  196. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
  197. }
  198. if (changed) {
  199. scene.update(void 0, true)
  200. const prevPickDirty = pickPass.pickDirty
  201. draw(true)
  202. pickPass.pickDirty = prevPickDirty // marking does not change picking buffers
  203. }
  204. }
  205. function render(force: boolean) {
  206. if (webgl.isContextLost) return false
  207. let didRender = false
  208. controls.update(currentTime)
  209. Viewport.set(camera.viewport, 0, 0, width, height)
  210. const cameraChanged = camera.update()
  211. multiSample.update(force || cameraChanged, currentTime)
  212. if (force || cameraChanged || multiSample.enabled) {
  213. renderer.setViewport(0, 0, width, height)
  214. if (multiSample.enabled) {
  215. multiSample.render(true, p.transparentBackground)
  216. } else {
  217. drawPass.render(!postprocessing.enabled, p.transparentBackground)
  218. if (postprocessing.enabled) postprocessing.render(true)
  219. }
  220. pickPass.pickDirty = true
  221. didRender = true
  222. }
  223. return didRender;
  224. }
  225. let forceNextDraw = false;
  226. let currentTime = 0;
  227. function draw(force?: boolean) {
  228. if (render(!!force || forceNextDraw)) {
  229. didDraw.next(now() - startTime as now.Timestamp)
  230. }
  231. forceNextDraw = false;
  232. drawPending = false
  233. }
  234. function requestDraw(force?: boolean) {
  235. if (drawPending) return
  236. drawPending = true
  237. forceNextDraw = !!force;
  238. }
  239. function animate() {
  240. currentTime = now();
  241. commit();
  242. camera.transition.tick(currentTime);
  243. draw(false);
  244. if (!camera.transition.inTransition && !webgl.isContextLost) {
  245. interactionHelper.tick(currentTime);
  246. }
  247. requestAnimationFrame(animate)
  248. }
  249. function identify(x: number, y: number): PickingId | undefined {
  250. return webgl.isContextLost ? undefined : pickPass.identify(x, y)
  251. }
  252. function commit(isSynchronous: boolean = false) {
  253. const allCommited = commitScene(isSynchronous);
  254. // Only reset the camera after the full scene has been commited.
  255. if (allCommited) resolveCameraReset();
  256. }
  257. function resolveCameraReset() {
  258. if (!cameraResetRequested) return;
  259. const { center, radius } = scene.boundingSphereVisible;
  260. if (radius > 0) {
  261. const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration
  262. const focus = camera.getFocus(center, radius);
  263. const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus;
  264. camera.setState(snapshot, duration);
  265. }
  266. nextCameraResetDuration = void 0;
  267. nextCameraResetSnapshot = void 0;
  268. cameraResetRequested = false;
  269. }
  270. const oldBoundingSphereVisible = Sphere3D();
  271. const cameraSphere = Sphere3D();
  272. function shouldResetCamera() {
  273. if (camera.state.radiusMax === 0) return true;
  274. let cameraSphereOverlapsNone = true
  275. Sphere3D.set(cameraSphere, camera.state.target, camera.state.radius)
  276. // check if any renderable has moved outside of the old bounding sphere
  277. // and if no renderable is overlapping with the camera sphere
  278. for (const r of scene.renderables) {
  279. if (!r.state.visible) continue;
  280. const b = r.values.boundingSphere.ref.value;
  281. if (!b.radius) continue;
  282. if (!Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
  283. if (Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false;
  284. }
  285. return cameraSphereOverlapsNone;
  286. }
  287. const sceneCommitTimeoutMs = 250;
  288. function commitScene(isSynchronous: boolean) {
  289. if (!scene.needsCommit) return true;
  290. // snapshot the current bounding sphere of visible objects
  291. Sphere3D.copy(oldBoundingSphereVisible, scene.boundingSphereVisible);
  292. if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
  293. if (debugHelper.isEnabled) debugHelper.update();
  294. if (reprCount.value === 0 || shouldResetCamera()) {
  295. cameraResetRequested = true;
  296. }
  297. if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
  298. camera.setState({ radiusMax: scene.boundingSphere.radius })
  299. reprCount.next(reprRenderObjects.size);
  300. return true;
  301. }
  302. function add(repr: Representation.Any) {
  303. registerAutoUpdate(repr);
  304. const oldRO = reprRenderObjects.get(repr)
  305. const newRO = new Set<GraphicsRenderObject>()
  306. repr.renderObjects.forEach(o => newRO.add(o))
  307. if (oldRO) {
  308. if (!SetUtils.areEqual(newRO, oldRO)) {
  309. newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o) })
  310. oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o) })
  311. }
  312. } else {
  313. repr.renderObjects.forEach(o => scene.add(o))
  314. }
  315. reprRenderObjects.set(repr, newRO)
  316. scene.update(repr.renderObjects, false)
  317. }
  318. function remove(repr: Representation.Any) {
  319. unregisterAutoUpdate(repr);
  320. const renderObjects = reprRenderObjects.get(repr)
  321. if (renderObjects) {
  322. renderObjects.forEach(o => scene.remove(o))
  323. reprRenderObjects.delete(repr)
  324. scene.update(repr.renderObjects, false, true)
  325. }
  326. }
  327. function registerAutoUpdate(repr: Representation.Any) {
  328. if (reprUpdatedSubscriptions.has(repr)) return;
  329. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  330. if (!repr.state.syncManually) add(repr);
  331. }))
  332. }
  333. function unregisterAutoUpdate(repr: Representation.Any) {
  334. const updatedSubscription = reprUpdatedSubscriptions.get(repr);
  335. if (updatedSubscription) {
  336. updatedSubscription.unsubscribe();
  337. reprUpdatedSubscriptions.delete(repr);
  338. }
  339. }
  340. handleResize()
  341. return {
  342. webgl,
  343. add,
  344. remove,
  345. commit,
  346. update: (repr, keepSphere) => {
  347. if (repr) {
  348. if (!reprRenderObjects.has(repr)) return;
  349. scene.update(repr.renderObjects, !!keepSphere);
  350. } else {
  351. scene.update(void 0, !!keepSphere)
  352. }
  353. },
  354. clear: () => {
  355. reprUpdatedSubscriptions.forEach(v => v.unsubscribe())
  356. reprUpdatedSubscriptions.clear()
  357. reprRenderObjects.clear()
  358. scene.clear()
  359. debugHelper.clear()
  360. requestDraw(true)
  361. reprCount.next(reprRenderObjects.size)
  362. },
  363. syncVisibility: () => {
  364. if (camera.state.radiusMax === 0) {
  365. cameraResetRequested = true
  366. nextCameraResetDuration = 0
  367. }
  368. if (scene.syncVisibility()) {
  369. if (debugHelper.isEnabled) debugHelper.update()
  370. }
  371. },
  372. // draw,
  373. requestDraw,
  374. animate,
  375. identify,
  376. mark,
  377. getLoci,
  378. handleResize,
  379. requestCameraReset: options => {
  380. nextCameraResetDuration = options?.durationMs;
  381. nextCameraResetSnapshot = options?.snapshot;
  382. cameraResetRequested = true;
  383. },
  384. camera,
  385. boundingSphere: scene.boundingSphere,
  386. downloadScreenshot: () => {
  387. // TODO
  388. },
  389. getPixelData: (variant: GraphicsRenderVariant) => {
  390. switch (variant) {
  391. case 'color': return webgl.getDrawingBufferPixelData()
  392. case 'pickObject': return pickPass.objectPickTarget.getPixelData()
  393. case 'pickInstance': return pickPass.instancePickTarget.getPixelData()
  394. case 'pickGroup': return pickPass.groupPickTarget.getPixelData()
  395. case 'depth': return readTexture(webgl, drawPass.depthTexture) as PixelData
  396. }
  397. },
  398. didDraw,
  399. reprCount,
  400. setProps: (props: Partial<Canvas3DProps>) => {
  401. const cameraState: Partial<Camera.Snapshot> = Object.create(null)
  402. if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
  403. cameraState.mode = props.camera.mode
  404. }
  405. if (props.cameraFog !== undefined) {
  406. const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0
  407. if (newFog !== camera.state.fog) cameraState.fog = newFog
  408. }
  409. if (props.cameraClipping !== undefined) {
  410. if (props.cameraClipping.far !== undefined && props.cameraClipping.far !== camera.state.clipFar) {
  411. cameraState.clipFar = props.cameraClipping.far
  412. }
  413. if (props.cameraClipping.radius !== undefined) {
  414. const radius = (scene.boundingSphere.radius / 100) * (100 - props.cameraClipping.radius)
  415. if (radius > 0 && radius !== cameraState.radius) {
  416. // if radius = 0, NaNs happen
  417. cameraState.radius = Math.max(radius, 0.01)
  418. }
  419. }
  420. }
  421. if (Object.keys(cameraState).length > 0) camera.setState(cameraState)
  422. if (props.camera?.helper) cameraHelper.setProps(props.camera.helper)
  423. if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs
  424. if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground
  425. if (props.postprocessing) postprocessing.setProps(props.postprocessing)
  426. if (props.multiSample) multiSample.setProps(props.multiSample)
  427. if (props.renderer) renderer.setProps(props.renderer)
  428. if (props.trackball) controls.setProps(props.trackball)
  429. if (props.debug) debugHelper.setProps(props.debug)
  430. requestDraw(true)
  431. },
  432. getImagePass: (props: Partial<ImageProps> = {}) => {
  433. return new ImagePass(webgl, renderer, scene, camera, debugHelper, cameraHelper, props)
  434. },
  435. get props() {
  436. const radius = scene.boundingSphere.radius > 0
  437. ? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
  438. : 0
  439. return {
  440. camera: {
  441. mode: camera.state.mode,
  442. helper: { ...cameraHelper.props }
  443. },
  444. cameraFog: camera.state.fog > 0
  445. ? { name: 'on' as const, params: { intensity: camera.state.fog } }
  446. : { name: 'off' as const, params: {} },
  447. cameraClipping: { far: camera.state.clipFar, radius },
  448. cameraResetDurationMs: p.cameraResetDurationMs,
  449. transparentBackground: p.transparentBackground,
  450. postprocessing: { ...postprocessing.props },
  451. multiSample: { ...multiSample.props },
  452. renderer: { ...renderer.props },
  453. trackball: { ...controls.props },
  454. debug: { ...debugHelper.props }
  455. }
  456. },
  457. get input() {
  458. return input
  459. },
  460. get stats() {
  461. return renderer.stats
  462. },
  463. get interaction() {
  464. return interactionHelper.events
  465. },
  466. dispose: () => {
  467. contextRestoredSub.unsubscribe()
  468. scene.clear()
  469. debugHelper.clear()
  470. input.dispose()
  471. controls.dispose()
  472. renderer.dispose()
  473. interactionHelper.dispose()
  474. }
  475. }
  476. function handleResize() {
  477. width = gl.drawingBufferWidth
  478. height = gl.drawingBufferHeight
  479. renderer.setViewport(0, 0, width, height)
  480. Viewport.set(camera.viewport, 0, 0, width, height)
  481. Viewport.set(controls.viewport, 0, 0, width, height)
  482. drawPass.setSize(width, height)
  483. pickPass.setSize(width, height)
  484. postprocessing.setSize(width, height)
  485. multiSample.setSize(width, height)
  486. requestDraw(true)
  487. }
  488. }
  489. }