canvas3d.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /**
  2. * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. */
  6. import { BehaviorSubject, Subscription } from 'rxjs';
  7. import { now } from '../mol-util/now';
  8. import { Vec3 } from '../mol-math/linear-algebra'
  9. import InputObserver, { ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer'
  10. import Renderer, { RendererStats, RendererParams } from '../mol-gl/renderer'
  11. import { GraphicsRenderObject } from '../mol-gl/render-object'
  12. import { TrackballControls, TrackballControlsParams } from './controls/trackball'
  13. import { Viewport } from './camera/util'
  14. import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
  15. import { Representation } from '../mol-repr/representation';
  16. import Scene from '../mol-gl/scene';
  17. import { GraphicsRenderVariant } from '../mol-gl/webgl/render-item';
  18. import { PickingId } from '../mol-geo/geometry/picking';
  19. import { MarkerAction } from '../mol-util/marker-action';
  20. import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
  21. import { Camera } from './camera';
  22. import { ParamDefinition as PD } from '../mol-util/param-definition';
  23. import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
  24. import { SetUtils } from '../mol-util/set';
  25. import { Canvas3dInteractionHelper } from './helper/interaction-events';
  26. import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
  27. import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
  28. import { GLRenderingContext } from '../mol-gl/webgl/compat';
  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. export const Canvas3DParams = {
  35. cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
  36. cameraFog: PD.Numeric(50, { min: 1, max: 100, step: 1 }),
  37. cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
  38. multiSample: PD.Group(MultiSampleParams),
  39. postprocessing: PD.Group(PostprocessingParams),
  40. renderer: PD.Group(RendererParams),
  41. trackball: PD.Group(TrackballControlsParams),
  42. debug: PD.Group(DebugHelperParams)
  43. }
  44. export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
  45. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  46. export { Canvas3D }
  47. interface Canvas3D {
  48. readonly webgl: WebGLContext,
  49. add: (repr: Representation.Any) => void
  50. remove: (repr: Representation.Any) => void
  51. update: (repr?: Representation.Any, keepBoundingSphere?: boolean) => void
  52. clear: () => void
  53. // draw: (force?: boolean) => void
  54. requestDraw: (force?: boolean) => void
  55. animate: () => void
  56. identify: (x: number, y: number) => PickingId | undefined
  57. mark: (loci: Representation.Loci, action: MarkerAction) => void
  58. getLoci: (pickingId: PickingId) => Representation.Loci
  59. readonly didDraw: BehaviorSubject<now.Timestamp>
  60. readonly reprCount: BehaviorSubject<number>
  61. handleResize: () => void
  62. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  63. resetCamera: () => void
  64. readonly camera: Camera
  65. downloadScreenshot: () => void
  66. getPixelData: (variant: GraphicsRenderVariant) => PixelData
  67. setProps: (props: Partial<Canvas3DProps>) => void
  68. /** Returns a copy of the current Canvas3D instance props */
  69. readonly props: Readonly<Canvas3DProps>
  70. readonly input: InputObserver
  71. readonly stats: RendererStats
  72. readonly interaction: Canvas3dInteractionHelper['events']
  73. dispose: () => void
  74. }
  75. const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()))
  76. const DefaultRunTask = (task: Task<unknown>) => task.run()
  77. namespace Canvas3D {
  78. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
  79. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
  80. export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) {
  81. const gl = getGLContext(canvas, {
  82. alpha: false,
  83. antialias: true,
  84. depth: true,
  85. preserveDrawingBuffer: true
  86. })
  87. if (gl === null) throw new Error('Could not create a WebGL rendering context')
  88. const input = InputObserver.fromElement(canvas)
  89. return Canvas3D.create(gl, input, props, runTask)
  90. }
  91. export function create(gl: GLRenderingContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D {
  92. const p = { ...DefaultCanvas3DParams, ...props }
  93. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
  94. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
  95. const reprCount = new BehaviorSubject(0)
  96. const startTime = now()
  97. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
  98. const webgl = createContext(gl)
  99. let width = gl.drawingBufferWidth
  100. let height = gl.drawingBufferHeight
  101. const scene = Scene.create(webgl)
  102. const camera = new Camera({
  103. position: Vec3.create(0, 0, 100),
  104. mode: p.cameraMode,
  105. fog: p.cameraFog
  106. })
  107. const controls = TrackballControls.create(input, camera, p.trackball)
  108. const renderer = Renderer.create(webgl, camera, p.renderer)
  109. const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
  110. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
  111. const drawPass = new DrawPass(webgl, renderer, scene, debugHelper)
  112. const pickPass = new PickPass(webgl, renderer, scene, 0.5)
  113. const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
  114. const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
  115. let drawPending = false
  116. let cameraResetRequested = false
  117. function getLoci(pickingId: PickingId) {
  118. let loci: Loci = EmptyLoci
  119. let repr: Representation.Any = Representation.Empty
  120. reprRenderObjects.forEach((_, _repr) => {
  121. const _loci = _repr.getLoci(pickingId)
  122. if (!isEmptyLoci(_loci)) {
  123. if (!isEmptyLoci(loci)) {
  124. console.warn('found another loci, this should not happen')
  125. }
  126. loci = _loci
  127. repr = _repr
  128. }
  129. })
  130. return { loci, repr }
  131. }
  132. function mark(reprLoci: Representation.Loci, action: MarkerAction) {
  133. const { repr, loci } = reprLoci
  134. let changed = false
  135. if (repr) {
  136. changed = repr.mark(loci, action)
  137. } else {
  138. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
  139. }
  140. if (changed) {
  141. scene.update(void 0, true)
  142. const prevPickDirty = pickPass.pickDirty
  143. draw(true)
  144. pickPass.pickDirty = prevPickDirty // marking does not change picking buffers
  145. }
  146. }
  147. function render(variant: 'pick' | 'draw', force: boolean) {
  148. if (scene.isCommiting) return false
  149. let didRender = false
  150. controls.update(currentTime)
  151. const cameraChanged = camera.update()
  152. multiSample.update(force || cameraChanged, currentTime)
  153. if (force || cameraChanged || multiSample.enabled) {
  154. switch (variant) {
  155. case 'pick':
  156. pickPass.render()
  157. break;
  158. case 'draw':
  159. renderer.setViewport(0, 0, width, height);
  160. if (multiSample.enabled) {
  161. multiSample.render()
  162. } else {
  163. drawPass.render(!postprocessing.enabled)
  164. if (postprocessing.enabled) postprocessing.render(true)
  165. }
  166. pickPass.pickDirty = true
  167. break;
  168. }
  169. didRender = true
  170. }
  171. return didRender && cameraChanged;
  172. }
  173. let forceNextDraw = false;
  174. let currentTime = 0;
  175. function draw(force?: boolean) {
  176. if (render('draw', !!force || forceNextDraw)) {
  177. didDraw.next(now() - startTime as now.Timestamp)
  178. }
  179. forceNextDraw = false;
  180. drawPending = false
  181. }
  182. function requestDraw(force?: boolean) {
  183. if (drawPending) return
  184. drawPending = true
  185. forceNextDraw = !!force;
  186. }
  187. function animate() {
  188. currentTime = now();
  189. camera.transition.tick(currentTime);
  190. draw(false);
  191. if (!camera.transition.inTransition) interactionHelper.tick(currentTime);
  192. requestAnimationFrame(animate)
  193. }
  194. function identify(x: number, y: number): PickingId | undefined {
  195. return pickPass.identify(x, y)
  196. }
  197. function commit(renderObjects?: readonly GraphicsRenderObject[]) {
  198. scene.update(renderObjects, false)
  199. runTask(scene.commit()).then(() => {
  200. if (cameraResetRequested && !scene.isCommiting) {
  201. camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius)
  202. cameraResetRequested = false
  203. }
  204. if (debugHelper.isEnabled) debugHelper.update()
  205. requestDraw(true)
  206. reprCount.next(reprRenderObjects.size)
  207. })
  208. }
  209. function add(repr: Representation.Any) {
  210. const oldRO = reprRenderObjects.get(repr)
  211. const newRO = new Set<GraphicsRenderObject>()
  212. repr.renderObjects.forEach(o => newRO.add(o))
  213. if (oldRO) {
  214. if (!SetUtils.areEqual(newRO, oldRO)) {
  215. for (const o of Array.from(newRO)) { if (!oldRO.has(o)) scene.add(o); }
  216. for (const o of Array.from(oldRO)) { if (!newRO.has(o)) scene.remove(o) }
  217. }
  218. } else {
  219. repr.renderObjects.forEach(o => scene.add(o))
  220. }
  221. reprRenderObjects.set(repr, newRO)
  222. commit(repr.renderObjects)
  223. }
  224. handleResize()
  225. return {
  226. webgl,
  227. add: (repr: Representation.Any) => {
  228. add(repr)
  229. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  230. if (!repr.state.syncManually) add(repr)
  231. }))
  232. },
  233. remove: (repr: Representation.Any) => {
  234. const updatedSubscription = reprUpdatedSubscriptions.get(repr)
  235. if (updatedSubscription) {
  236. updatedSubscription.unsubscribe()
  237. }
  238. const renderObjects = reprRenderObjects.get(repr)
  239. if (renderObjects) {
  240. renderObjects.forEach(o => scene.remove(o))
  241. reprRenderObjects.delete(repr)
  242. commit()
  243. }
  244. },
  245. update: (repr, keepSphere) => {
  246. if (repr) {
  247. if (!reprRenderObjects.has(repr)) return;
  248. scene.update(repr.renderObjects, !!keepSphere);
  249. } else {
  250. scene.update(void 0, !!keepSphere)
  251. }
  252. },
  253. clear: () => {
  254. reprRenderObjects.clear()
  255. scene.clear()
  256. debugHelper.clear()
  257. requestDraw(true)
  258. reprCount.next(reprRenderObjects.size)
  259. },
  260. // draw,
  261. requestDraw,
  262. animate,
  263. identify,
  264. mark,
  265. getLoci,
  266. handleResize,
  267. resetCamera: (/*dir?: Vec3*/) => {
  268. if (scene.isCommiting) {
  269. cameraResetRequested = true
  270. } else {
  271. camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius, p.cameraResetDurationMs)
  272. requestDraw(true);
  273. }
  274. },
  275. camera,
  276. downloadScreenshot: () => {
  277. // TODO
  278. },
  279. getPixelData: (variant: GraphicsRenderVariant) => {
  280. switch (variant) {
  281. case 'color': return webgl.getDrawingBufferPixelData()
  282. case 'pickObject': return pickPass.objectPickTarget.getPixelData()
  283. case 'pickInstance': return pickPass.instancePickTarget.getPixelData()
  284. case 'pickGroup': return pickPass.groupPickTarget.getPixelData()
  285. case 'depth': return readTexture(webgl, drawPass.depthTexture) as PixelData
  286. }
  287. },
  288. didDraw,
  289. reprCount,
  290. setProps: (props: Partial<Canvas3DProps>) => {
  291. if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
  292. camera.setState({ mode: props.cameraMode })
  293. }
  294. if (props.cameraFog !== undefined && props.cameraFog !== camera.state.fog) {
  295. camera.setState({ fog: props.cameraFog })
  296. }
  297. if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs
  298. if (props.postprocessing) postprocessing.setProps(props.postprocessing)
  299. if (props.multiSample) multiSample.setProps(props.multiSample)
  300. if (props.renderer) renderer.setProps(props.renderer)
  301. if (props.trackball) controls.setProps(props.trackball)
  302. if (props.debug) debugHelper.setProps(props.debug)
  303. requestDraw(true)
  304. },
  305. get props() {
  306. return {
  307. cameraMode: camera.state.mode,
  308. cameraFog: camera.state.fog,
  309. cameraResetDurationMs: p.cameraResetDurationMs,
  310. postprocessing: { ...postprocessing.props },
  311. multiSample: { ...multiSample.props },
  312. renderer: { ...renderer.props },
  313. trackball: { ...controls.props },
  314. debug: { ...debugHelper.props }
  315. }
  316. },
  317. get input() {
  318. return input
  319. },
  320. get stats() {
  321. return renderer.stats
  322. },
  323. get interaction() {
  324. return interactionHelper.events
  325. },
  326. dispose: () => {
  327. scene.clear()
  328. debugHelper.clear()
  329. input.dispose()
  330. controls.dispose()
  331. renderer.dispose()
  332. interactionHelper.dispose()
  333. }
  334. }
  335. function handleResize() {
  336. width = gl.drawingBufferWidth
  337. height = gl.drawingBufferHeight
  338. renderer.setViewport(0, 0, width, height)
  339. Viewport.set(camera.viewport, 0, 0, width, height)
  340. Viewport.set(controls.viewport, 0, 0, width, height)
  341. drawPass.setSize(width, height)
  342. pickPass.setSize(width, height)
  343. postprocessing.setSize(width, height)
  344. multiSample.setSize(width, height)
  345. requestDraw(true)
  346. }
  347. }
  348. }