canvas3d.ts 15 KB

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