canvas3d.ts 16 KB

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