canvas3d.ts 17 KB

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