canvas3d.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. /**
  2. * Copyright (c) 2018 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 } 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 { resizeCanvas } from './util';
  15. import { createContext, getGLContext, WebGLContext } from 'mol-gl/webgl/context';
  16. import { Representation } from 'mol-repr/representation';
  17. import { createRenderTarget } from 'mol-gl/webgl/render-target';
  18. import Scene from 'mol-gl/scene';
  19. import { RenderVariant } from 'mol-gl/webgl/render-item';
  20. import { PickingId } from 'mol-geo/geometry/picking';
  21. import { MarkerAction } from 'mol-geo/geometry/marker-data';
  22. import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
  23. import { Color } from 'mol-util/color';
  24. import { Camera } from './camera';
  25. import { ParamDefinition as PD } from 'mol-util/param-definition';
  26. import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
  27. import { decodeFloatRGB } from 'mol-util/float-packing';
  28. import { SetUtils } from 'mol-util/set';
  29. import { Canvas3dInteractionHelper } from './helper/interaction-events';
  30. export const Canvas3DParams = {
  31. // TODO: FPS cap?
  32. // maxFps: PD.Numeric(30),
  33. cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
  34. backgroundColor: PD.Color(Color(0x000000)),
  35. 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.' }),
  36. clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }),
  37. fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }),
  38. pickingAlphaThreshold: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'The minimum opacity value needed for an object to be pickable.' }),
  39. trackball: PD.Group(TrackballControlsParams),
  40. debug: PD.Group(DebugHelperParams)
  41. }
  42. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  43. export { Canvas3D }
  44. interface Canvas3D {
  45. readonly webgl: WebGLContext,
  46. add: (repr: Representation.Any) => void
  47. remove: (repr: Representation.Any) => void
  48. update: (repr?: Representation.Any, keepBoundingSphere?: boolean) => void
  49. clear: () => void
  50. // draw: (force?: boolean) => void
  51. requestDraw: (force?: boolean) => void
  52. animate: () => void
  53. pick: () => void
  54. identify: (x: number, y: number) => Promise<PickingId | undefined>
  55. mark: (loci: Representation.Loci, action: MarkerAction) => void
  56. getLoci: (pickingId: PickingId) => Representation.Loci
  57. readonly didDraw: BehaviorSubject<now.Timestamp>
  58. handleResize: () => void
  59. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  60. resetCamera: () => void
  61. readonly camera: Camera
  62. downloadScreenshot: () => void
  63. getImageData: (variant: RenderVariant) => ImageData
  64. setProps: (props: Partial<Canvas3DProps>) => void
  65. /** Returns a copy of the current Canvas3D instance props */
  66. readonly props: Canvas3DProps
  67. readonly input: InputObserver
  68. readonly stats: RendererStats
  69. readonly interaction: Canvas3dInteractionHelper['events']
  70. dispose: () => void
  71. }
  72. namespace Canvas3D {
  73. export interface HighlightEvent { current: Representation.Loci, prev: Representation.Loci, modifiers?: ModifiersKeys }
  74. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
  75. export function create(canvas: HTMLCanvasElement, container: Element, props: Partial<Canvas3DProps> = {}): Canvas3D {
  76. const p = { ...PD.getDefaultValues(Canvas3DParams), ...props }
  77. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
  78. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
  79. const reprCount = new BehaviorSubject(0)
  80. const startTime = now()
  81. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
  82. const input = InputObserver.create(canvas)
  83. const camera = new Camera({
  84. near: 0.1,
  85. far: 10000,
  86. position: Vec3.create(0, 0, 10),
  87. mode: p.cameraMode
  88. })
  89. const gl = getGLContext(canvas, {
  90. alpha: false,
  91. antialias: true,
  92. depth: true,
  93. preserveDrawingBuffer: true
  94. })
  95. if (gl === null) {
  96. throw new Error('Could not create a WebGL rendering context')
  97. }
  98. const webgl = createContext(gl)
  99. const scene = Scene.create(webgl)
  100. const controls = TrackballControls.create(input, camera, p.trackball)
  101. const renderer = Renderer.create(webgl, camera, { clearColor: p.backgroundColor })
  102. let pickScale = 0.25 / webgl.pixelRatio
  103. let pickWidth = Math.round(canvas.width * pickScale)
  104. let pickHeight = Math.round(canvas.height * pickScale)
  105. const objectPickTarget = createRenderTarget(webgl, pickWidth, pickHeight)
  106. const instancePickTarget = createRenderTarget(webgl, pickWidth, pickHeight)
  107. const groupPickTarget = createRenderTarget(webgl, pickWidth, pickHeight)
  108. let pickDirty = true
  109. let isIdentifying = false
  110. let isUpdating = false
  111. let drawPending = false
  112. const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
  113. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
  114. function getLoci(pickingId: PickingId) {
  115. let loci: Loci = EmptyLoci
  116. let repr: Representation.Any = Representation.Empty
  117. reprRenderObjects.forEach((_, _repr) => {
  118. const _loci = _repr.getLoci(pickingId)
  119. if (!isEmptyLoci(_loci)) {
  120. if (!isEmptyLoci(loci)) console.warn('found another loci')
  121. loci = _loci
  122. repr = _repr
  123. }
  124. })
  125. return { loci, repr }
  126. }
  127. function mark(reprLoci: Representation.Loci, action: MarkerAction) {
  128. const { repr, loci } = reprLoci
  129. let changed = false
  130. if (repr) {
  131. changed = repr.mark(loci, action)
  132. } else {
  133. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
  134. }
  135. if (changed) {
  136. scene.update(void 0, true)
  137. const prevPickDirty = pickDirty
  138. draw(true)
  139. pickDirty = prevPickDirty // marking does not change picking buffers
  140. }
  141. }
  142. let currentNear = -1, currentFar = -1, currentFogNear = -1, currentFogFar = -1
  143. function setClipping() {
  144. const cDist = Vec3.distance(camera.state.position, camera.state.target)
  145. const bRadius = Math.max(10, scene.boundingSphere.radius)
  146. const nearFactor = (50 - p.clip[0]) / 50
  147. const farFactor = -(50 - p.clip[1]) / 50
  148. let near = cDist - (bRadius * nearFactor)
  149. let far = cDist + (bRadius * farFactor)
  150. const fogNearFactor = (50 - p.fog[0]) / 50
  151. const fogFarFactor = -(50 - p.fog[1]) / 50
  152. let fogNear = cDist - (bRadius * fogNearFactor)
  153. let fogFar = cDist + (bRadius * fogFarFactor)
  154. if (camera.state.mode === 'perspective') {
  155. near = Math.max(0.1, p.cameraClipDistance, near)
  156. far = Math.max(1, far)
  157. fogNear = Math.max(0.1, fogNear)
  158. fogFar = Math.max(1, fogFar)
  159. } else if (camera.state.mode === 'orthographic') {
  160. if (p.cameraClipDistance > 0) {
  161. near = Math.max(p.cameraClipDistance, near)
  162. }
  163. }
  164. if (near !== currentNear || far !== currentFar || fogNear !== currentFogNear || fogFar !== currentFogFar) {
  165. camera.setState({ near, far, fogNear, fogFar })
  166. currentNear = near, currentFar = far, currentFogNear = fogNear, currentFogFar = fogFar
  167. }
  168. }
  169. function render(variant: 'pick' | 'draw', force: boolean) {
  170. if (isIdentifying || isUpdating) return false
  171. let didRender = false
  172. controls.update(currentTime);
  173. // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane.
  174. if (!camera.transition.inTransition) setClipping();
  175. const cameraChanged = camera.updateMatrices();
  176. if (force || cameraChanged) {
  177. switch (variant) {
  178. case 'pick':
  179. renderer.setViewport(0, 0, pickWidth, pickHeight);
  180. objectPickTarget.bind();
  181. renderer.clear()
  182. renderer.render(scene, 'pickObject');
  183. instancePickTarget.bind();
  184. renderer.clear()
  185. renderer.render(scene, 'pickInstance');
  186. groupPickTarget.bind();
  187. renderer.clear()
  188. renderer.render(scene, 'pickGroup');
  189. break;
  190. case 'draw':
  191. webgl.unbindFramebuffer();
  192. renderer.setViewport(0, 0, canvas.width, canvas.height);
  193. renderer.clear()
  194. renderer.render(scene, variant);
  195. if (debugHelper.isEnabled) {
  196. debugHelper.syncVisibility()
  197. renderer.render(debugHelper.scene, 'draw')
  198. }
  199. pickDirty = true
  200. break;
  201. }
  202. didRender = true
  203. }
  204. return didRender && cameraChanged;
  205. }
  206. let forceNextDraw = false;
  207. let currentTime = 0;
  208. function draw(force?: boolean) {
  209. if (render('draw', !!force || forceNextDraw)) {
  210. didDraw.next(now() - startTime as now.Timestamp)
  211. }
  212. forceNextDraw = false;
  213. drawPending = false
  214. }
  215. function requestDraw(force?: boolean) {
  216. if (drawPending) return
  217. drawPending = true
  218. forceNextDraw = !!force;
  219. }
  220. function animate() {
  221. currentTime = now();
  222. camera.transition.tick(currentTime);
  223. draw(false);
  224. if (!camera.transition.inTransition) interactionHelper.tick(currentTime);
  225. window.requestAnimationFrame(animate)
  226. }
  227. function pick() {
  228. if (pickDirty) {
  229. render('pick', true)
  230. pickDirty = false
  231. }
  232. }
  233. async function identify(x: number, y: number): Promise<PickingId | undefined> {
  234. if (isIdentifying) return
  235. pick() // must be called before setting `isIdentifying = true`
  236. isIdentifying = true
  237. x *= webgl.pixelRatio
  238. y *= webgl.pixelRatio
  239. y = canvas.height - y // flip y
  240. const buffer = new Uint8Array(4)
  241. const xp = Math.round(x * pickScale)
  242. const yp = Math.round(y * pickScale)
  243. objectPickTarget.bind()
  244. // TODO slow in Chrome, ok in FF; doesn't play well with gpu surface calc
  245. // await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
  246. webgl.readPixels(xp, yp, 1, 1, buffer)
  247. const objectId = decodeFloatRGB(buffer[0], buffer[1], buffer[2])
  248. if (objectId === -1) { isIdentifying = false; return; }
  249. instancePickTarget.bind()
  250. // await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
  251. webgl.readPixels(xp, yp, 1, 1, buffer)
  252. const instanceId = decodeFloatRGB(buffer[0], buffer[1], buffer[2])
  253. if (instanceId === -1) { isIdentifying = false; return; }
  254. groupPickTarget.bind()
  255. // await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
  256. webgl.readPixels(xp, yp, 1, 1, buffer)
  257. const groupId = decodeFloatRGB(buffer[0], buffer[1], buffer[2])
  258. if (groupId === -1) { isIdentifying = false; return; }
  259. isIdentifying = false
  260. return { objectId, instanceId, groupId }
  261. }
  262. function add(repr: Representation.Any) {
  263. isUpdating = true
  264. const oldRO = reprRenderObjects.get(repr)
  265. const newRO = new Set<GraphicsRenderObject>()
  266. repr.renderObjects.forEach(o => newRO.add(o))
  267. if (oldRO) {
  268. if (!SetUtils.areEqual(newRO, oldRO)) {
  269. for (const o of Array.from(newRO)) { if (!oldRO.has(o)) scene.add(o); }
  270. for (const o of Array.from(oldRO)) { if (!newRO.has(o)) scene.remove(o) }
  271. }
  272. } else {
  273. repr.renderObjects.forEach(o => scene.add(o))
  274. }
  275. reprRenderObjects.set(repr, newRO)
  276. scene.update(repr.renderObjects, false)
  277. if (debugHelper.isEnabled) debugHelper.update()
  278. isUpdating = false
  279. requestDraw(true)
  280. reprCount.next(reprRenderObjects.size)
  281. }
  282. handleResize()
  283. return {
  284. webgl,
  285. add: (repr: Representation.Any) => {
  286. add(repr)
  287. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  288. if (!repr.state.syncManually) add(repr)
  289. }))
  290. },
  291. remove: (repr: Representation.Any) => {
  292. const updatedSubscription = reprUpdatedSubscriptions.get(repr)
  293. if (updatedSubscription) {
  294. updatedSubscription.unsubscribe()
  295. }
  296. const renderObjects = reprRenderObjects.get(repr)
  297. if (renderObjects) {
  298. isUpdating = true
  299. renderObjects.forEach(o => scene.remove(o))
  300. reprRenderObjects.delete(repr)
  301. scene.update(void 0, false)
  302. if (debugHelper.isEnabled) debugHelper.update()
  303. isUpdating = false
  304. requestDraw(true)
  305. reprCount.next(reprRenderObjects.size)
  306. }
  307. },
  308. update: (repr, keepSphere) => {
  309. if (repr) {
  310. if (!reprRenderObjects.has(repr)) return;
  311. scene.update(repr.renderObjects, !!keepSphere);
  312. } else {
  313. scene.update(void 0, !!keepSphere)
  314. }
  315. },
  316. clear: () => {
  317. reprRenderObjects.clear()
  318. scene.clear()
  319. debugHelper.clear()
  320. },
  321. // draw,
  322. requestDraw,
  323. animate,
  324. pick,
  325. identify,
  326. mark,
  327. getLoci,
  328. handleResize,
  329. resetCamera: () => {
  330. camera.focus(scene.boundingSphere.center, scene.boundingSphere.radius)
  331. requestDraw(true);
  332. },
  333. camera,
  334. downloadScreenshot: () => {
  335. // TODO
  336. },
  337. getImageData: (variant: RenderVariant) => {
  338. switch (variant) {
  339. case 'draw': return renderer.getImageData()
  340. case 'pickObject': return objectPickTarget.getImageData()
  341. case 'pickInstance': return instancePickTarget.getImageData()
  342. case 'pickGroup': return groupPickTarget.getImageData()
  343. }
  344. },
  345. didDraw,
  346. setProps: (props: Partial<Canvas3DProps>) => {
  347. if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
  348. camera.setState({ mode: props.cameraMode })
  349. }
  350. if (props.backgroundColor !== undefined && props.backgroundColor !== renderer.props.clearColor) {
  351. renderer.setClearColor(props.backgroundColor)
  352. }
  353. if (props.cameraClipDistance !== undefined) p.cameraClipDistance = props.cameraClipDistance
  354. if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]]
  355. if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]]
  356. if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== renderer.props.pickingAlphaThreshold) {
  357. renderer.setPickingAlphaThreshold(props.pickingAlphaThreshold)
  358. }
  359. if (props.trackball) controls.setProps(props.trackball)
  360. if (props.debug) debugHelper.setProps(props.debug)
  361. requestDraw(true)
  362. },
  363. get props() {
  364. return {
  365. cameraMode: camera.state.mode,
  366. backgroundColor: renderer.props.clearColor,
  367. cameraClipDistance: p.cameraClipDistance,
  368. clip: p.clip,
  369. fog: p.fog,
  370. pickingAlphaThreshold: renderer.props.pickingAlphaThreshold,
  371. trackball: { ...controls.props },
  372. debug: { ...debugHelper.props }
  373. }
  374. },
  375. get input() {
  376. return input
  377. },
  378. get stats() {
  379. return renderer.stats
  380. },
  381. get interaction() {
  382. return interactionHelper.events
  383. },
  384. dispose: () => {
  385. scene.clear()
  386. debugHelper.clear()
  387. input.dispose()
  388. controls.dispose()
  389. renderer.dispose()
  390. camera.dispose()
  391. interactionHelper.dispose()
  392. }
  393. }
  394. function handleResize() {
  395. resizeCanvas(canvas, container)
  396. renderer.setViewport(0, 0, canvas.width, canvas.height)
  397. Viewport.set(camera.viewport, 0, 0, canvas.width, canvas.height)
  398. Viewport.set(controls.viewport, 0, 0, canvas.width, canvas.height)
  399. pickScale = 0.25 / webgl.pixelRatio
  400. pickWidth = Math.round(canvas.width * pickScale)
  401. pickHeight = Math.round(canvas.height * pickScale)
  402. objectPickTarget.setSize(pickWidth, pickHeight)
  403. instancePickTarget.setSize(pickWidth, pickHeight)
  404. groupPickTarget.setSize(pickWidth, pickHeight)
  405. requestDraw(true)
  406. }
  407. }
  408. }