canvas3d.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  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 { Task } from '../mol-task';
  34. import { ImagePass, ImageProps } from './passes/image';
  35. import { Sphere3D } from '../mol-math/geometry';
  36. import { isDebugMode } from '../mol-util/debug';
  37. export const Canvas3DParams = {
  38. cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
  39. cameraFog: PD.Numeric(50, { min: 0, max: 100, step: 1 }),
  40. cameraClipFar: PD.Boolean(true),
  41. cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
  42. transparentBackground: PD.Boolean(false),
  43. multiSample: PD.Group(MultiSampleParams),
  44. postprocessing: PD.Group(PostprocessingParams),
  45. renderer: PD.Group(RendererParams),
  46. trackball: PD.Group(TrackballControlsParams),
  47. debug: PD.Group(DebugHelperParams)
  48. }
  49. export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
  50. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  51. export { Canvas3D }
  52. interface Canvas3D {
  53. readonly webgl: WebGLContext,
  54. add(repr: Representation.Any): void
  55. remove(repr: Representation.Any): void
  56. /**
  57. * This function must be called if animate() is not set up so that add/remove actions take place.
  58. */
  59. commit(isSynchronous?: boolean): void
  60. update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
  61. clear(): void
  62. requestDraw(force?: boolean): void
  63. animate(): void
  64. identify(x: number, y: number): PickingId | undefined
  65. mark(loci: Representation.Loci, action: MarkerAction): void
  66. getLoci(pickingId: PickingId): Representation.Loci
  67. readonly didDraw: BehaviorSubject<now.Timestamp>
  68. readonly reprCount: BehaviorSubject<number>
  69. handleResize(): void
  70. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  71. requestCameraReset(durationMs?: number): void
  72. readonly camera: Camera
  73. readonly boundingSphere: Readonly<Sphere3D>
  74. downloadScreenshot(): void
  75. getPixelData(variant: GraphicsRenderVariant): PixelData
  76. setProps(props: Partial<Canvas3DProps>): void
  77. getImagePass(): ImagePass
  78. /** Returns a copy of the current Canvas3D instance props */
  79. readonly props: Readonly<Canvas3DProps>
  80. readonly input: InputObserver
  81. readonly stats: RendererStats
  82. readonly interaction: Canvas3dInteractionHelper['events']
  83. dispose(): void
  84. }
  85. const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()))
  86. const DefaultRunTask = (task: Task<unknown>) => task.run()
  87. namespace Canvas3D {
  88. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  89. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
  90. export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) {
  91. const gl = getGLContext(canvas, {
  92. alpha: true,
  93. antialias: true,
  94. depth: true,
  95. preserveDrawingBuffer: true,
  96. premultipliedAlpha: false,
  97. })
  98. if (gl === null) throw new Error('Could not create a WebGL rendering context')
  99. const input = InputObserver.fromElement(canvas)
  100. const webgl = createContext(gl)
  101. if (isDebugMode) {
  102. const loseContextExt = gl.getExtension('WEBGL_lose_context')
  103. if (loseContextExt) {
  104. canvas.addEventListener('mousedown', e => {
  105. if (webgl.isContextLost) return
  106. if (!e.shiftKey || !e.ctrlKey || !e.altKey) return
  107. console.log('lose context')
  108. loseContextExt.loseContext()
  109. setTimeout(() => {
  110. if (!webgl.isContextLost) return
  111. console.log('restore context')
  112. loseContextExt.restoreContext()
  113. }, 1000)
  114. }, false)
  115. }
  116. }
  117. // https://www.khronos.org/webgl/wiki/HandlingContextLost
  118. canvas.addEventListener('webglcontextlost', e => {
  119. webgl.setContextLost()
  120. e.preventDefault()
  121. if (isDebugMode) console.log('context lost')
  122. }, false)
  123. canvas.addEventListener('webglcontextrestored', () => {
  124. if (!webgl.isContextLost) return
  125. webgl.handleContextRestored()
  126. if (isDebugMode) console.log('context restored')
  127. }, false)
  128. return Canvas3D.create(webgl, input, props, runTask)
  129. }
  130. export function create(webgl: WebGLContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D {
  131. const p = { ...DefaultCanvas3DParams, ...props }
  132. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
  133. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
  134. const reprCount = new BehaviorSubject(0)
  135. const startTime = now()
  136. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
  137. const { gl, contextRestored } = webgl
  138. let width = gl.drawingBufferWidth
  139. let height = gl.drawingBufferHeight
  140. const scene = Scene.create(webgl)
  141. const camera = new Camera({
  142. position: Vec3.create(0, 0, 100),
  143. mode: p.cameraMode,
  144. fog: p.cameraFog,
  145. clipFar: p.cameraClipFar
  146. })
  147. const controls = TrackballControls.create(input, camera, p.trackball)
  148. const renderer = Renderer.create(webgl, p.renderer)
  149. const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
  150. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
  151. const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper)
  152. const pickPass = new PickPass(webgl, renderer, scene, camera, 0.5)
  153. const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
  154. const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
  155. const contextRestoredSub = contextRestored.subscribe(() => {
  156. pickPass.pickDirty = true
  157. draw(true)
  158. })
  159. let drawPending = false
  160. let cameraResetRequested = false
  161. let nextCameraResetDuration: number | undefined = void 0
  162. function getLoci(pickingId: PickingId) {
  163. let loci: Loci = EmptyLoci
  164. let repr: Representation.Any = Representation.Empty
  165. reprRenderObjects.forEach((_, _repr) => {
  166. const _loci = _repr.getLoci(pickingId)
  167. if (!isEmptyLoci(_loci)) {
  168. if (!isEmptyLoci(loci)) {
  169. console.warn('found another loci, this should not happen')
  170. }
  171. loci = _loci
  172. repr = _repr
  173. }
  174. })
  175. return { loci, repr }
  176. }
  177. function mark(reprLoci: Representation.Loci, action: MarkerAction) {
  178. const { repr, loci } = reprLoci
  179. let changed = false
  180. if (repr) {
  181. changed = repr.mark(loci, action)
  182. } else {
  183. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
  184. }
  185. if (changed) {
  186. scene.update(void 0, true)
  187. const prevPickDirty = pickPass.pickDirty
  188. draw(true)
  189. pickPass.pickDirty = prevPickDirty // marking does not change picking buffers
  190. }
  191. }
  192. function render(force: boolean) {
  193. if (webgl.isContextLost) return false
  194. let didRender = false
  195. controls.update(currentTime)
  196. Viewport.set(camera.viewport, 0, 0, width, height)
  197. const cameraChanged = camera.update()
  198. multiSample.update(force || cameraChanged, currentTime)
  199. if (force || cameraChanged || multiSample.enabled) {
  200. renderer.setViewport(0, 0, width, height)
  201. if (multiSample.enabled) {
  202. multiSample.render(true, p.transparentBackground)
  203. } else {
  204. drawPass.render(!postprocessing.enabled, p.transparentBackground)
  205. if (postprocessing.enabled) postprocessing.render(true)
  206. }
  207. pickPass.pickDirty = true
  208. didRender = true
  209. }
  210. return didRender;
  211. }
  212. let forceNextDraw = false;
  213. let currentTime = 0;
  214. function draw(force?: boolean) {
  215. if (render(!!force || forceNextDraw)) {
  216. didDraw.next(now() - startTime as now.Timestamp)
  217. }
  218. forceNextDraw = false;
  219. drawPending = false
  220. }
  221. function requestDraw(force?: boolean) {
  222. if (drawPending) return
  223. drawPending = true
  224. forceNextDraw = !!force;
  225. }
  226. function animate() {
  227. currentTime = now();
  228. commit();
  229. camera.transition.tick(currentTime);
  230. draw(false);
  231. if (!camera.transition.inTransition && !webgl.isContextLost) {
  232. interactionHelper.tick(currentTime);
  233. }
  234. requestAnimationFrame(animate)
  235. }
  236. function identify(x: number, y: number): PickingId | undefined {
  237. return webgl.isContextLost ? undefined : pickPass.identify(x, y)
  238. }
  239. function commit(isSynchronous: boolean = false) {
  240. const allCommited = commitScene(isSynchronous);
  241. // Only reset the camera after the full scene has been commited.
  242. if (allCommited) resolveCameraReset();
  243. }
  244. function resolveCameraReset() {
  245. if (!cameraResetRequested) return;
  246. const { center, radius } = scene.boundingSphere;
  247. const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration
  248. camera.focus(center, radius, radius, duration);
  249. nextCameraResetDuration = void 0;
  250. cameraResetRequested = false;
  251. }
  252. const sceneCommitTimeoutMs = 250;
  253. function commitScene(isSynchronous: boolean) {
  254. if (!scene.needsCommit) return true;
  255. if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
  256. if (debugHelper.isEnabled) debugHelper.update();
  257. reprCount.next(reprRenderObjects.size);
  258. return true;
  259. }
  260. function add(repr: Representation.Any) {
  261. registerAutoUpdate(repr);
  262. const oldRO = reprRenderObjects.get(repr)
  263. const newRO = new Set<GraphicsRenderObject>()
  264. repr.renderObjects.forEach(o => newRO.add(o))
  265. if (oldRO) {
  266. if (!SetUtils.areEqual(newRO, oldRO)) {
  267. newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o) })
  268. oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o) })
  269. }
  270. } else {
  271. repr.renderObjects.forEach(o => scene.add(o))
  272. }
  273. reprRenderObjects.set(repr, newRO)
  274. scene.update(repr.renderObjects, false)
  275. }
  276. function remove(repr: Representation.Any) {
  277. unregisterAutoUpdate(repr);
  278. const renderObjects = reprRenderObjects.get(repr)
  279. if (renderObjects) {
  280. renderObjects.forEach(o => scene.remove(o))
  281. reprRenderObjects.delete(repr)
  282. scene.update(repr.renderObjects, false, true)
  283. }
  284. }
  285. function registerAutoUpdate(repr: Representation.Any) {
  286. if (reprUpdatedSubscriptions.has(repr)) return;
  287. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  288. if (!repr.state.syncManually) add(repr);
  289. }))
  290. }
  291. function unregisterAutoUpdate(repr: Representation.Any) {
  292. const updatedSubscription = reprUpdatedSubscriptions.get(repr);
  293. if (updatedSubscription) {
  294. updatedSubscription.unsubscribe();
  295. reprUpdatedSubscriptions.delete(repr);
  296. }
  297. }
  298. handleResize()
  299. return {
  300. webgl,
  301. add,
  302. remove,
  303. commit,
  304. update: (repr, keepSphere) => {
  305. if (repr) {
  306. if (!reprRenderObjects.has(repr)) return;
  307. scene.update(repr.renderObjects, !!keepSphere);
  308. } else {
  309. scene.update(void 0, !!keepSphere)
  310. }
  311. },
  312. clear: () => {
  313. reprUpdatedSubscriptions.forEach(v => v.unsubscribe())
  314. reprUpdatedSubscriptions.clear()
  315. reprRenderObjects.clear()
  316. scene.clear()
  317. debugHelper.clear()
  318. requestDraw(true)
  319. reprCount.next(reprRenderObjects.size)
  320. },
  321. // draw,
  322. requestDraw,
  323. animate,
  324. identify,
  325. mark,
  326. getLoci,
  327. handleResize,
  328. requestCameraReset: (durationMs) => {
  329. nextCameraResetDuration = durationMs;
  330. cameraResetRequested = true;
  331. },
  332. camera,
  333. boundingSphere: scene.boundingSphere,
  334. downloadScreenshot: () => {
  335. // TODO
  336. },
  337. getPixelData: (variant: GraphicsRenderVariant) => {
  338. switch (variant) {
  339. case 'color': return webgl.getDrawingBufferPixelData()
  340. case 'pickObject': return pickPass.objectPickTarget.getPixelData()
  341. case 'pickInstance': return pickPass.instancePickTarget.getPixelData()
  342. case 'pickGroup': return pickPass.groupPickTarget.getPixelData()
  343. case 'depth': return readTexture(webgl, drawPass.depthTexture) as PixelData
  344. }
  345. },
  346. didDraw,
  347. reprCount,
  348. setProps: (props: Partial<Canvas3DProps>) => {
  349. if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
  350. camera.setState({ mode: props.cameraMode })
  351. }
  352. if (props.cameraFog !== undefined && props.cameraFog !== camera.state.fog) {
  353. camera.setState({ fog: props.cameraFog })
  354. }
  355. if (props.cameraClipFar !== undefined && props.cameraClipFar !== camera.state.clipFar) {
  356. camera.setState({ clipFar: props.cameraClipFar })
  357. }
  358. if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs
  359. if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground
  360. if (props.postprocessing) postprocessing.setProps(props.postprocessing)
  361. if (props.multiSample) multiSample.setProps(props.multiSample)
  362. if (props.renderer) renderer.setProps(props.renderer)
  363. if (props.trackball) controls.setProps(props.trackball)
  364. if (props.debug) debugHelper.setProps(props.debug)
  365. requestDraw(true)
  366. },
  367. getImagePass: (props: Partial<ImageProps> = {}) => {
  368. return new ImagePass(webgl, renderer, scene, camera, debugHelper, props)
  369. },
  370. get props() {
  371. return {
  372. cameraMode: camera.state.mode,
  373. cameraFog: camera.state.fog,
  374. cameraClipFar: camera.state.clipFar,
  375. cameraResetDurationMs: p.cameraResetDurationMs,
  376. transparentBackground: p.transparentBackground,
  377. postprocessing: { ...postprocessing.props },
  378. multiSample: { ...multiSample.props },
  379. renderer: { ...renderer.props },
  380. trackball: { ...controls.props },
  381. debug: { ...debugHelper.props }
  382. }
  383. },
  384. get input() {
  385. return input
  386. },
  387. get stats() {
  388. return renderer.stats
  389. },
  390. get interaction() {
  391. return interactionHelper.events
  392. },
  393. dispose: () => {
  394. contextRestoredSub.unsubscribe()
  395. scene.clear()
  396. debugHelper.clear()
  397. input.dispose()
  398. controls.dispose()
  399. renderer.dispose()
  400. interactionHelper.dispose()
  401. }
  402. }
  403. function handleResize() {
  404. width = gl.drawingBufferWidth
  405. height = gl.drawingBufferHeight
  406. renderer.setViewport(0, 0, width, height)
  407. Viewport.set(camera.viewport, 0, 0, width, height)
  408. Viewport.set(controls.viewport, 0, 0, width, height)
  409. drawPass.setSize(width, height)
  410. pickPass.setSize(width, height)
  411. postprocessing.setSize(width, height)
  412. multiSample.setSize(width, height)
  413. requestDraw(true)
  414. }
  415. }
  416. }