canvas3d.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  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. // TODO: FPS cap?
  36. // maxFps: PD.Numeric(30),
  37. cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
  38. 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.' }),
  39. clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }),
  40. fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }),
  41. multiSample: PD.Group(MultiSampleParams),
  42. postprocessing: PD.Group(PostprocessingParams),
  43. renderer: PD.Group(RendererParams),
  44. trackball: PD.Group(TrackballControlsParams),
  45. debug: PD.Group(DebugHelperParams)
  46. }
  47. export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
  48. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  49. export { Canvas3D }
  50. interface Canvas3D {
  51. readonly webgl: WebGLContext,
  52. add: (repr: Representation.Any) => void
  53. remove: (repr: Representation.Any) => void
  54. update: (repr?: Representation.Any, keepBoundingSphere?: boolean) => void
  55. clear: () => void
  56. // draw: (force?: boolean) => void
  57. requestDraw: (force?: boolean) => void
  58. animate: () => void
  59. identify: (x: number, y: number) => PickingId | undefined
  60. mark: (loci: Representation.Loci, action: MarkerAction) => void
  61. getLoci: (pickingId: PickingId) => Representation.Loci
  62. readonly didDraw: BehaviorSubject<now.Timestamp>
  63. readonly reprCount: BehaviorSubject<number>
  64. handleResize: () => void
  65. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  66. resetCamera: () => void
  67. readonly camera: Camera
  68. downloadScreenshot: () => void
  69. getPixelData: (variant: GraphicsRenderVariant) => PixelData
  70. setProps: (props: Partial<Canvas3DProps>) => void
  71. /** Returns a copy of the current Canvas3D instance props */
  72. readonly props: Readonly<Canvas3DProps>
  73. readonly input: InputObserver
  74. readonly stats: RendererStats
  75. readonly interaction: Canvas3dInteractionHelper['events']
  76. dispose: () => void
  77. }
  78. const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()))
  79. const DefaultRunTask = (task: Task<unknown>) => task.run()
  80. namespace Canvas3D {
  81. export interface HighlightEvent { current: Representation.Loci, modifiers?: ModifiersKeys }
  82. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
  83. export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask) {
  84. const gl = getGLContext(canvas, {
  85. alpha: false,
  86. antialias: true,
  87. depth: true,
  88. preserveDrawingBuffer: true
  89. })
  90. if (gl === null) throw new Error('Could not create a WebGL rendering context')
  91. const input = InputObserver.fromElement(canvas)
  92. return Canvas3D.create(gl, input, props, runTask)
  93. }
  94. export function create(gl: GLRenderingContext, input: InputObserver, props: Partial<Canvas3DProps> = {}, runTask = DefaultRunTask): Canvas3D {
  95. const p = { ...DefaultCanvas3DParams, ...props }
  96. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
  97. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
  98. const reprCount = new BehaviorSubject(0)
  99. const startTime = now()
  100. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
  101. const camera = new Camera({
  102. near: 0.1,
  103. far: 10000,
  104. position: Vec3.create(0, 0, 100),
  105. mode: p.cameraMode
  106. })
  107. const webgl = createContext(gl)
  108. let width = gl.drawingBufferWidth
  109. let height = gl.drawingBufferHeight
  110. const scene = Scene.create(webgl)
  111. const controls = TrackballControls.create(input, camera, p.trackball)
  112. const renderer = Renderer.create(webgl, camera, p.renderer)
  113. const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
  114. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
  115. const drawPass = new DrawPass(webgl, renderer, scene, debugHelper)
  116. const pickPass = new PickPass(webgl, renderer, scene, 0.5)
  117. const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing)
  118. const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample)
  119. let drawPending = false
  120. let cameraResetRequested: boolean | Vec3 = false
  121. function getLoci(pickingId: PickingId) {
  122. let loci: Loci = EmptyLoci
  123. let repr: Representation.Any = Representation.Empty
  124. reprRenderObjects.forEach((_, _repr) => {
  125. const _loci = _repr.getLoci(pickingId)
  126. if (!isEmptyLoci(_loci)) {
  127. if (!isEmptyLoci(loci)) console.warn('found another loci')
  128. loci = _loci
  129. repr = _repr
  130. }
  131. })
  132. return { loci, repr }
  133. }
  134. function mark(reprLoci: Representation.Loci, action: MarkerAction) {
  135. const { repr, loci } = reprLoci
  136. let changed = false
  137. if (repr) {
  138. changed = repr.mark(loci, action)
  139. } else {
  140. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
  141. }
  142. if (changed) {
  143. scene.update(void 0, true)
  144. const prevPickDirty = pickPass.pickDirty
  145. draw(true)
  146. pickPass.pickDirty = prevPickDirty // marking does not change picking buffers
  147. }
  148. }
  149. let currentNear = -1, currentFar = -1, currentFogNear = -1, currentFogFar = -1
  150. function setClipping() {
  151. const cDist = Vec3.distance(camera.state.position, camera.state.target)
  152. const bRadius = Math.max(10, scene.boundingSphere.radius)
  153. const nearFactor = (50 - p.clip[0]) / 50
  154. const farFactor = -(50 - p.clip[1]) / 50
  155. let near = cDist - (bRadius * nearFactor)
  156. let far = cDist + (bRadius * farFactor)
  157. const fogNearFactor = (50 - p.fog[0]) / 50
  158. const fogFarFactor = -(50 - p.fog[1]) / 50
  159. let fogNear = cDist - (bRadius * fogNearFactor)
  160. let fogFar = cDist + (bRadius * fogFarFactor)
  161. if (camera.state.mode === 'perspective') {
  162. near = Math.max(1, p.cameraClipDistance, near)
  163. far = Math.max(1, far)
  164. fogNear = Math.max(1, fogNear)
  165. fogFar = Math.max(1, fogFar)
  166. } else if (camera.state.mode === 'orthographic') {
  167. if (p.cameraClipDistance > 0) {
  168. near = Math.max(p.cameraClipDistance, near)
  169. }
  170. }
  171. if (near !== currentNear || far !== currentFar || fogNear !== currentFogNear || fogFar !== currentFogFar) {
  172. camera.setState({ near, far, fogNear, fogFar })
  173. currentNear = near, currentFar = far, currentFogNear = fogNear, currentFogFar = fogFar
  174. }
  175. }
  176. function render(variant: 'pick' | 'draw', force: boolean) {
  177. if (scene.isCommiting) return false
  178. let didRender = false
  179. controls.update(currentTime);
  180. // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane.
  181. if (!camera.transition.inTransition) setClipping();
  182. const cameraChanged = camera.updateMatrices();
  183. multiSample.update(force || cameraChanged, currentTime)
  184. if (force || cameraChanged || multiSample.enabled) {
  185. switch (variant) {
  186. case 'pick':
  187. pickPass.render()
  188. break;
  189. case 'draw':
  190. renderer.setViewport(0, 0, width, height);
  191. if (multiSample.enabled) {
  192. multiSample.render()
  193. } else {
  194. drawPass.render(!postprocessing.enabled)
  195. if (postprocessing.enabled) postprocessing.render(true)
  196. }
  197. pickPass.pickDirty = true
  198. break;
  199. }
  200. didRender = true
  201. }
  202. return didRender && cameraChanged;
  203. }
  204. let forceNextDraw = false;
  205. let currentTime = 0;
  206. function draw(force?: boolean) {
  207. if (render('draw', !!force || forceNextDraw)) {
  208. didDraw.next(now() - startTime as now.Timestamp)
  209. }
  210. forceNextDraw = false;
  211. drawPending = false
  212. }
  213. function requestDraw(force?: boolean) {
  214. if (drawPending) return
  215. drawPending = true
  216. forceNextDraw = !!force;
  217. }
  218. function animate() {
  219. currentTime = now();
  220. camera.transition.tick(currentTime);
  221. draw(false);
  222. if (!camera.transition.inTransition) interactionHelper.tick(currentTime);
  223. requestAnimationFrame(animate)
  224. }
  225. function identify(x: number, y: number): PickingId | undefined {
  226. return pickPass.identify(x, y)
  227. }
  228. function commit(renderObjects?: readonly GraphicsRenderObject[]) {
  229. scene.update(renderObjects, false)
  230. runTask(scene.commit()).then(() => {
  231. if (cameraResetRequested && !scene.isCommiting) {
  232. const dir = typeof cameraResetRequested === 'boolean' ? undefined : cameraResetRequested
  233. camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius, dir)
  234. cameraResetRequested = false
  235. }
  236. if (debugHelper.isEnabled) debugHelper.update()
  237. requestDraw(true)
  238. reprCount.next(reprRenderObjects.size)
  239. })
  240. }
  241. function add(repr: Representation.Any) {
  242. const oldRO = reprRenderObjects.get(repr)
  243. const newRO = new Set<GraphicsRenderObject>()
  244. repr.renderObjects.forEach(o => newRO.add(o))
  245. if (oldRO) {
  246. if (!SetUtils.areEqual(newRO, oldRO)) {
  247. for (const o of Array.from(newRO)) { if (!oldRO.has(o)) scene.add(o); }
  248. for (const o of Array.from(oldRO)) { if (!newRO.has(o)) scene.remove(o) }
  249. }
  250. } else {
  251. repr.renderObjects.forEach(o => scene.add(o))
  252. }
  253. reprRenderObjects.set(repr, newRO)
  254. commit(repr.renderObjects)
  255. }
  256. handleResize()
  257. return {
  258. webgl,
  259. add: (repr: Representation.Any) => {
  260. add(repr)
  261. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  262. if (!repr.state.syncManually) add(repr)
  263. }))
  264. },
  265. remove: (repr: Representation.Any) => {
  266. const updatedSubscription = reprUpdatedSubscriptions.get(repr)
  267. if (updatedSubscription) {
  268. updatedSubscription.unsubscribe()
  269. }
  270. const renderObjects = reprRenderObjects.get(repr)
  271. if (renderObjects) {
  272. renderObjects.forEach(o => scene.remove(o))
  273. reprRenderObjects.delete(repr)
  274. commit()
  275. }
  276. },
  277. update: (repr, keepSphere) => {
  278. if (repr) {
  279. if (!reprRenderObjects.has(repr)) return;
  280. scene.update(repr.renderObjects, !!keepSphere);
  281. } else {
  282. scene.update(void 0, !!keepSphere)
  283. }
  284. },
  285. clear: () => {
  286. reprRenderObjects.clear()
  287. scene.clear()
  288. debugHelper.clear()
  289. },
  290. // draw,
  291. requestDraw,
  292. animate,
  293. identify,
  294. mark,
  295. getLoci,
  296. handleResize,
  297. resetCamera: (dir?: Vec3) => {
  298. if (scene.isCommiting) {
  299. cameraResetRequested = dir || true
  300. } else {
  301. camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius, dir)
  302. requestDraw(true);
  303. }
  304. },
  305. camera,
  306. downloadScreenshot: () => {
  307. // TODO
  308. },
  309. getPixelData: (variant: GraphicsRenderVariant) => {
  310. switch (variant) {
  311. case 'color': return webgl.getDrawingBufferPixelData()
  312. case 'pickObject': return pickPass.objectPickTarget.getPixelData()
  313. case 'pickInstance': return pickPass.instancePickTarget.getPixelData()
  314. case 'pickGroup': return pickPass.groupPickTarget.getPixelData()
  315. case 'depth': return readTexture(webgl, drawPass.depthTexture) as PixelData
  316. }
  317. },
  318. didDraw,
  319. reprCount,
  320. setProps: (props: Partial<Canvas3DProps>) => {
  321. if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
  322. camera.setState({ mode: props.cameraMode })
  323. }
  324. if (props.cameraClipDistance !== undefined) p.cameraClipDistance = props.cameraClipDistance
  325. if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]]
  326. if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]]
  327. if (props.postprocessing) postprocessing.setProps(props.postprocessing)
  328. if (props.multiSample) multiSample.setProps(props.multiSample)
  329. if (props.renderer) renderer.setProps(props.renderer)
  330. if (props.trackball) controls.setProps(props.trackball)
  331. if (props.debug) debugHelper.setProps(props.debug)
  332. requestDraw(true)
  333. },
  334. get props() {
  335. return {
  336. cameraMode: camera.state.mode,
  337. cameraClipDistance: p.cameraClipDistance,
  338. clip: p.clip,
  339. fog: p.fog,
  340. postprocessing: { ...postprocessing.props },
  341. multiSample: { ...multiSample.props },
  342. renderer: { ...renderer.props },
  343. trackball: { ...controls.props },
  344. debug: { ...debugHelper.props }
  345. }
  346. },
  347. get input() {
  348. return input
  349. },
  350. get stats() {
  351. return renderer.stats
  352. },
  353. get interaction() {
  354. return interactionHelper.events
  355. },
  356. dispose: () => {
  357. scene.clear()
  358. debugHelper.clear()
  359. input.dispose()
  360. controls.dispose()
  361. renderer.dispose()
  362. camera.dispose()
  363. interactionHelper.dispose()
  364. }
  365. }
  366. function handleResize() {
  367. width = gl.drawingBufferWidth
  368. height = gl.drawingBufferHeight
  369. renderer.setViewport(0, 0, width, height)
  370. Viewport.set(camera.viewport, 0, 0, width, height)
  371. Viewport.set(controls.viewport, 0, 0, width, height)
  372. drawPass.setSize(width, height)
  373. pickPass.setSize(width, height)
  374. postprocessing.setSize(width, height)
  375. multiSample.setSize(width, height)
  376. requestDraw(true)
  377. }
  378. }
  379. }