viewer.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  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 } from 'rxjs';
  7. import { Vec3, Mat4, EPSILON } from 'mol-math/linear-algebra'
  8. import InputObserver from 'mol-util/input/input-observer'
  9. import * as SetUtils from 'mol-util/set'
  10. import Renderer, { RendererStats } from 'mol-gl/renderer'
  11. import { RenderObject } from 'mol-gl/render-object'
  12. import TrackballControls from './controls/trackball'
  13. import { Viewport } from './camera/util'
  14. import { PerspectiveCamera } from './camera/perspective'
  15. import { resizeCanvas } from './util';
  16. import { createContext } from 'mol-gl/webgl/context';
  17. import { Representation } from 'mol-geo/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, decodeIdRGBA } from 'mol-geo/util/picking';
  22. import { labelFirst } from './label';
  23. import { FlagAction } from 'mol-geo/util/flag-data';
  24. import { EveryLoci } from 'mol-model/loci';
  25. interface Viewer {
  26. center: (p: Vec3) => void
  27. hide: (repr: Representation<any>) => void
  28. show: (repr: Representation<any>) => void
  29. add: (repr: Representation<any>) => void
  30. remove: (repr: Representation<any>) => void
  31. update: () => void
  32. clear: () => void
  33. draw: (force?: boolean) => void
  34. requestDraw: () => void
  35. animate: () => void
  36. pick: () => void
  37. identify: (x: number, y: number) => void
  38. reprCount: BehaviorSubject<number>
  39. identified: BehaviorSubject<string>
  40. didDraw: BehaviorSubject<number>
  41. handleResize: () => void
  42. resetCamera: () => void
  43. downloadScreenshot: () => void
  44. getImageData: (variant: RenderVariant) => ImageData
  45. input: InputObserver
  46. stats: RendererStats
  47. dispose: () => void
  48. }
  49. function getWebGLContext(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes) {
  50. function getContext(contextId: 'webgl' | 'experimental-webgl') {
  51. try {
  52. return canvas.getContext(contextId, contextAttributes)
  53. } catch (e) {
  54. return null
  55. }
  56. }
  57. return getContext('webgl') || getContext('experimental-webgl')
  58. }
  59. namespace Viewer {
  60. export function create(canvas: HTMLCanvasElement, container: Element): Viewer {
  61. const reprMap = new Map<Representation<any>, Set<RenderObject>>()
  62. const reprCount = new BehaviorSubject(0)
  63. const identified = new BehaviorSubject('')
  64. const startTime = performance.now()
  65. const didDraw = new BehaviorSubject(0)
  66. const input = InputObserver.create(canvas)
  67. input.resize.subscribe(handleResize)
  68. input.move.subscribe(({x, y}) => {
  69. const p = identify(x, y)
  70. let label = ''
  71. reprMap.forEach((roSet, repr) => {
  72. repr.applyFlags(EveryLoci, FlagAction.RemoveHighlight)
  73. const loci = repr.getLoci(p)
  74. if (loci) {
  75. label = labelFirst(loci)
  76. repr.applyFlags(loci, FlagAction.Highlight)
  77. }
  78. scene.update()
  79. requestDraw()
  80. })
  81. identified.next(`Object: ${p.objectId}, Instance: ${p.instanceId}, Element: ${p.elementId}, Label: ${label}`)
  82. })
  83. input.click.subscribe(({x, y}) => {
  84. const p = identify(x, y)
  85. reprMap.forEach((roSet, repr) => {
  86. const loci = repr.getLoci(p)
  87. if (loci) {
  88. repr.applyFlags(loci, FlagAction.ToggleSelect)
  89. scene.update()
  90. requestDraw()
  91. }
  92. })
  93. })
  94. const camera = PerspectiveCamera.create({
  95. near: 0.1,
  96. far: 10000,
  97. position: Vec3.create(0, 0, 50)
  98. })
  99. const controls = TrackballControls.create(input, camera, {
  100. })
  101. const gl = getWebGLContext(canvas, {
  102. alpha: false,
  103. antialias: true,
  104. depth: true,
  105. preserveDrawingBuffer: true
  106. })
  107. if (gl === null) {
  108. throw new Error('Could not create a WebGL rendering context')
  109. }
  110. const ctx = createContext(gl)
  111. const scene = Scene.create(ctx)
  112. const renderer = Renderer.create(ctx, camera)
  113. const pickScale = 1 / 4
  114. const pickWidth = Math.round(canvas.width * pickScale)
  115. const pickHeight = Math.round(canvas.height * pickScale)
  116. const objectPickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
  117. const instancePickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
  118. const elementPickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
  119. let pickDirty = true
  120. let drawPending = false
  121. const prevProjectionView = Mat4.zero()
  122. function render(variant: RenderVariant, force?: boolean) {
  123. let didRender = false
  124. controls.update()
  125. camera.update()
  126. if (force || !Mat4.areEqual(camera.projectionView, prevProjectionView, EPSILON.Value)) {
  127. Mat4.copy(prevProjectionView, camera.projectionView)
  128. renderer.render(scene, variant)
  129. if (variant === 'draw') {
  130. pickDirty = true
  131. pick()
  132. }
  133. didRender = true
  134. }
  135. return didRender
  136. }
  137. function draw(force?: boolean) {
  138. ctx.unbindFramebuffer()
  139. const viewport = { x: 0, y: 0, width: canvas.width, height: canvas.height }
  140. renderer.setViewport(viewport)
  141. if (render('draw', force)) {
  142. didDraw.next(performance.now() - startTime)
  143. }
  144. drawPending = false
  145. }
  146. function requestDraw () {
  147. if (drawPending) return
  148. drawPending = true
  149. window.requestAnimationFrame(() => draw(true))
  150. }
  151. function animate () {
  152. draw(false)
  153. window.requestAnimationFrame(() => animate())
  154. }
  155. function pick() {
  156. objectPickTarget.bind()
  157. render('pickObject', pickDirty)
  158. instancePickTarget.bind()
  159. render('pickInstance', pickDirty)
  160. elementPickTarget.bind()
  161. render('pickElement', pickDirty)
  162. pickDirty = false
  163. }
  164. function identify (x: number, y: number): PickingId {
  165. x *= ctx.pixelRatio
  166. y *= ctx.pixelRatio
  167. y = canvas.height - y // flip y
  168. const buffer = new Uint8Array(4)
  169. const xp = Math.round(x * pickScale)
  170. const yp = Math.round(y * pickScale)
  171. objectPickTarget.bind()
  172. ctx.readPixels(xp, yp, 1, 1, buffer)
  173. const objectId = decodeIdRGBA(buffer[0], buffer[1], buffer[2])
  174. instancePickTarget.bind()
  175. ctx.readPixels(xp, yp, 1, 1, buffer)
  176. const instanceId = decodeIdRGBA(buffer[0], buffer[1], buffer[2])
  177. elementPickTarget.bind()
  178. ctx.readPixels(xp, yp, 1, 1, buffer)
  179. const elementId = decodeIdRGBA(buffer[0], buffer[1], buffer[2])
  180. return { objectId, instanceId, elementId }
  181. }
  182. handleResize()
  183. return {
  184. center: (p: Vec3) => {
  185. Vec3.set(controls.target, p[0], p[1], p[2])
  186. },
  187. hide: (repr: Representation<any>) => {
  188. const renderObjectSet = reprMap.get(repr)
  189. if (renderObjectSet) renderObjectSet.forEach(o => o.state.visible = false)
  190. },
  191. show: (repr: Representation<any>) => {
  192. const renderObjectSet = reprMap.get(repr)
  193. if (renderObjectSet) renderObjectSet.forEach(o => o.state.visible = true)
  194. },
  195. add: (repr: Representation<any>) => {
  196. const oldRO = reprMap.get(repr)
  197. const newRO = new Set<RenderObject>()
  198. repr.renderObjects.forEach(o => newRO.add(o))
  199. if (oldRO) {
  200. SetUtils.difference(newRO, oldRO).forEach(o => scene.add(o))
  201. SetUtils.difference(oldRO, newRO).forEach(o => scene.remove(o))
  202. scene.update()
  203. } else {
  204. repr.renderObjects.forEach(o => scene.add(o))
  205. }
  206. reprMap.set(repr, newRO)
  207. reprCount.next(reprMap.size)
  208. },
  209. remove: (repr: Representation<any>) => {
  210. const renderObjectSet = reprMap.get(repr)
  211. if (renderObjectSet) {
  212. renderObjectSet.forEach(o => scene.remove(o))
  213. reprMap.delete(repr)
  214. reprCount.next(reprMap.size)
  215. }
  216. },
  217. update: () => scene.update(),
  218. clear: () => {
  219. reprMap.clear()
  220. scene.clear()
  221. },
  222. draw,
  223. requestDraw,
  224. animate,
  225. pick,
  226. identify,
  227. handleResize,
  228. resetCamera: () => {
  229. // TODO
  230. },
  231. downloadScreenshot: () => {
  232. // TODO
  233. },
  234. getImageData: (variant: RenderVariant) => {
  235. switch (variant) {
  236. case 'draw': return renderer.getImageData()
  237. case 'pickObject': return objectPickTarget.getImageData()
  238. case 'pickInstance': return instancePickTarget.getImageData()
  239. case 'pickElement': return elementPickTarget.getImageData()
  240. }
  241. },
  242. reprCount,
  243. identified,
  244. didDraw,
  245. get input() {
  246. return input
  247. },
  248. get stats() {
  249. return renderer.stats
  250. },
  251. dispose: () => {
  252. scene.clear()
  253. input.dispose()
  254. controls.dispose()
  255. renderer.dispose()
  256. }
  257. }
  258. function handleResize() {
  259. resizeCanvas(canvas, container)
  260. const viewport = { x: 0, y: 0, width: canvas.width, height: canvas.height }
  261. renderer.setViewport(viewport)
  262. Viewport.copy(camera.viewport, viewport)
  263. Viewport.copy(controls.viewport, viewport)
  264. const pickWidth = Math.round(canvas.width * pickScale)
  265. const pickHeight = Math.round(canvas.height * pickScale)
  266. objectPickTarget.setSize(pickWidth, pickHeight)
  267. instancePickTarget.setSize(pickWidth, pickHeight)
  268. elementPickTarget.setSize(pickWidth, pickHeight)
  269. }
  270. }
  271. }
  272. export default Viewer