viewer.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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, getGLContext, Context } 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, 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. interface Viewer {
  26. webgl: Context,
  27. center: (p: Vec3) => void
  28. hide: (repr: Representation<any>) => void
  29. show: (repr: Representation<any>) => void
  30. add: (repr: Representation<any>) => void
  31. remove: (repr: Representation<any>) => void
  32. update: () => void
  33. clear: () => void
  34. draw: (force?: boolean) => void
  35. requestDraw: (force?: boolean) => void
  36. animate: () => void
  37. pick: () => void
  38. identify: (x: number, y: number) => PickingId | undefined
  39. mark: (loci: Loci, action: MarkerAction) => void
  40. getLoci: (pickingId: PickingId) => Loci
  41. reprCount: BehaviorSubject<number>
  42. identified: BehaviorSubject<string>
  43. didDraw: BehaviorSubject<number>
  44. handleResize: () => void
  45. resetCamera: () => void
  46. downloadScreenshot: () => void
  47. getImageData: (variant: RenderVariant) => ImageData
  48. input: InputObserver
  49. stats: RendererStats
  50. dispose: () => void
  51. }
  52. namespace Viewer {
  53. export function create(canvas: HTMLCanvasElement, container: Element): Viewer {
  54. const reprMap = new Map<Representation<any>, Set<RenderObject>>()
  55. const reprCount = new BehaviorSubject(0)
  56. const identified = new BehaviorSubject('')
  57. const startTime = performance.now()
  58. const didDraw = new BehaviorSubject(0)
  59. const input = InputObserver.create(canvas)
  60. const camera = PerspectiveCamera.create({
  61. near: 0.1,
  62. far: 10000,
  63. position: Vec3.create(0, 0, 50)
  64. })
  65. // camera.lookAt(Vec3.create(0, 0, 0))
  66. const gl = getGLContext(canvas, {
  67. alpha: false,
  68. antialias: true,
  69. depth: true,
  70. preserveDrawingBuffer: true
  71. })
  72. if (gl === null) {
  73. throw new Error('Could not create a WebGL rendering context')
  74. }
  75. const ctx = createContext(gl)
  76. const scene = Scene.create(ctx)
  77. const controls = TrackballControls.create(input, camera, {})
  78. const renderer = Renderer.create(ctx, camera, { clearColor: Color(0x000000) })
  79. const pickScale = 1
  80. const pickWidth = Math.round(canvas.width * pickScale)
  81. const pickHeight = Math.round(canvas.height * pickScale)
  82. const objectPickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
  83. const instancePickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
  84. const groupPickTarget = createRenderTarget(ctx, pickWidth, pickHeight)
  85. let pickDirty = true
  86. let isPicking = false
  87. let drawPending = false
  88. let lastRenderTime = -1
  89. const prevProjectionView = Mat4.zero()
  90. const prevSceneView = Mat4.zero()
  91. function getLoci(pickingId: PickingId) {
  92. let loci: Loci = EmptyLoci
  93. reprMap.forEach((_, repr) => {
  94. const _loci = repr.getLoci(pickingId)
  95. if (!isEmptyLoci(_loci)) {
  96. if (!isEmptyLoci(loci)) console.warn('found another loci')
  97. loci = _loci
  98. }
  99. })
  100. return loci
  101. }
  102. function mark(loci: Loci, action: MarkerAction) {
  103. let changed = false
  104. reprMap.forEach((roSet, repr) => {
  105. changed = repr.mark(loci, action) || changed
  106. })
  107. if (changed) {
  108. // console.log('changed')
  109. scene.update()
  110. draw(true)
  111. pickDirty = false // picking buffers should not have changed
  112. }
  113. }
  114. let nearPlaneDelta = 0
  115. function computeNearDistance() {
  116. const focusRadius = scene.boundingSphere.radius
  117. let dist = Vec3.distance(controls.target, camera.position)
  118. if (dist > focusRadius) return dist - focusRadius
  119. return 0
  120. }
  121. function render(variant: RenderVariant, force?: boolean) {
  122. if (isPicking) return false
  123. // const p = scene.boundingSphere.center
  124. // console.log(p[0], p[1], p[2])
  125. // Vec3.set(controls.target, p[0], p[1], p[2])
  126. const focusRadius = scene.boundingSphere.radius
  127. const targetDistance = Vec3.distance(controls.target, camera.position)
  128. // console.log(targetDistance, controls.target, camera.position)
  129. let near = computeNearDistance() + nearPlaneDelta
  130. camera.near = Math.max(0.01, Math.min(near, targetDistance - 0.5))
  131. let fogNear = targetDistance - camera.near + 1 * focusRadius - nearPlaneDelta;
  132. let fogFar = targetDistance - camera.near + 2 * focusRadius - nearPlaneDelta;
  133. // console.log(fogNear, fogFar);
  134. camera.fogNear = Math.max(fogNear, 0.1);
  135. camera.fogFar = Math.max(fogFar, 0.2);
  136. // console.log(camera.fogNear, camera.fogFar, targetDistance)
  137. switch (variant) {
  138. case 'pickObject': objectPickTarget.bind(); break;
  139. case 'pickInstance': instancePickTarget.bind(); break;
  140. case 'pickGroup': groupPickTarget.bind(); break;
  141. case 'draw':
  142. ctx.unbindFramebuffer();
  143. renderer.setViewport(0, 0, canvas.width, canvas.height);
  144. break;
  145. }
  146. let didRender = false
  147. controls.update()
  148. camera.update()
  149. if (force || !Mat4.areEqual(camera.projectionView, prevProjectionView, EPSILON.Value) || !Mat4.areEqual(scene.view, prevSceneView, EPSILON.Value)) {
  150. // console.log('foo', force, prevSceneView, scene.view)
  151. Mat4.copy(prevProjectionView, camera.projectionView)
  152. Mat4.copy(prevSceneView, scene.view)
  153. renderer.render(scene, variant)
  154. if (variant === 'draw') {
  155. lastRenderTime = performance.now()
  156. pickDirty = true
  157. }
  158. didRender = true
  159. }
  160. return didRender
  161. }
  162. function draw(force?: boolean) {
  163. if (render('draw', force)) {
  164. didDraw.next(performance.now() - startTime)
  165. }
  166. drawPending = false
  167. }
  168. function requestDraw(force?: boolean) {
  169. if (drawPending) return
  170. drawPending = true
  171. window.requestAnimationFrame(() => draw(force))
  172. }
  173. function animate() {
  174. draw(false)
  175. if (performance.now() - lastRenderTime > 200) {
  176. if (pickDirty) pick()
  177. }
  178. window.requestAnimationFrame(() => animate())
  179. }
  180. function pick() {
  181. render('pickObject', pickDirty)
  182. render('pickInstance', pickDirty)
  183. render('pickGroup', pickDirty)
  184. pickDirty = false
  185. }
  186. function identify(x: number, y: number): PickingId | undefined {
  187. if (pickDirty) return undefined
  188. isPicking = true
  189. x *= ctx.pixelRatio
  190. y *= ctx.pixelRatio
  191. y = canvas.height - y // flip y
  192. const buffer = new Uint8Array(4)
  193. const xp = Math.round(x * pickScale)
  194. const yp = Math.round(y * pickScale)
  195. objectPickTarget.bind()
  196. ctx.readPixels(xp, yp, 1, 1, buffer)
  197. const objectId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
  198. instancePickTarget.bind()
  199. ctx.readPixels(xp, yp, 1, 1, buffer)
  200. const instanceId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
  201. groupPickTarget.bind()
  202. ctx.readPixels(xp, yp, 1, 1, buffer)
  203. const groupId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
  204. isPicking = false
  205. return { objectId, instanceId, groupId }
  206. }
  207. handleResize()
  208. return {
  209. webgl: ctx,
  210. center: (p: Vec3) => {
  211. Vec3.set(controls.target, p[0], p[1], p[2])
  212. },
  213. hide: (repr: Representation<any>) => {
  214. const renderObjectSet = reprMap.get(repr)
  215. if (renderObjectSet) renderObjectSet.forEach(o => o.state.visible = false)
  216. },
  217. show: (repr: Representation<any>) => {
  218. const renderObjectSet = reprMap.get(repr)
  219. if (renderObjectSet) renderObjectSet.forEach(o => o.state.visible = true)
  220. },
  221. add: (repr: Representation<any>) => {
  222. const oldRO = reprMap.get(repr)
  223. const newRO = new Set<RenderObject>()
  224. repr.renderObjects.forEach(o => newRO.add(o))
  225. if (oldRO) {
  226. SetUtils.difference(newRO, oldRO).forEach(o => scene.add(o))
  227. SetUtils.difference(oldRO, newRO).forEach(o => scene.remove(o))
  228. scene.update()
  229. } else {
  230. repr.renderObjects.forEach(o => scene.add(o))
  231. }
  232. reprMap.set(repr, newRO)
  233. reprCount.next(reprMap.size)
  234. scene.update()
  235. },
  236. remove: (repr: Representation<any>) => {
  237. const renderObjectSet = reprMap.get(repr)
  238. if (renderObjectSet) {
  239. renderObjectSet.forEach(o => scene.remove(o))
  240. reprMap.delete(repr)
  241. reprCount.next(reprMap.size)
  242. scene.update()
  243. }
  244. },
  245. update: () => scene.update(),
  246. clear: () => {
  247. reprMap.clear()
  248. scene.clear()
  249. },
  250. draw,
  251. requestDraw,
  252. animate,
  253. pick,
  254. identify,
  255. mark,
  256. getLoci,
  257. handleResize,
  258. resetCamera: () => {
  259. // TODO
  260. },
  261. downloadScreenshot: () => {
  262. // TODO
  263. },
  264. getImageData: (variant: RenderVariant) => {
  265. switch (variant) {
  266. case 'draw': return renderer.getImageData()
  267. case 'pickObject': return objectPickTarget.getImageData()
  268. case 'pickInstance': return instancePickTarget.getImageData()
  269. case 'pickGroup': return groupPickTarget.getImageData()
  270. }
  271. },
  272. reprCount,
  273. identified,
  274. didDraw,
  275. get input() {
  276. return input
  277. },
  278. get stats() {
  279. return renderer.stats
  280. },
  281. dispose: () => {
  282. scene.clear()
  283. input.dispose()
  284. controls.dispose()
  285. renderer.dispose()
  286. }
  287. }
  288. function handleResize() {
  289. resizeCanvas(canvas, container)
  290. renderer.setViewport(0, 0, canvas.width, canvas.height)
  291. Viewport.set(camera.viewport, 0, 0, canvas.width, canvas.height)
  292. Viewport.set(controls.viewport, 0, 0, canvas.width, canvas.height)
  293. const pickWidth = Math.round(canvas.width * pickScale)
  294. const pickHeight = Math.round(canvas.height * pickScale)
  295. objectPickTarget.setSize(pickWidth, pickHeight)
  296. instancePickTarget.setSize(pickWidth, pickHeight)
  297. groupPickTarget.setSize(pickWidth, pickHeight)
  298. }
  299. }
  300. }
  301. export default Viewer