canvas3d.ts 13 KB

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