canvas3d.ts 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967
  1. /**
  2. * Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  5. * @author David Sehnal <david.sehnal@gmail.com>
  6. * @author Gianluca Tomasello <giagitom@gmail.com>
  7. */
  8. import { BehaviorSubject, Subscription } from 'rxjs';
  9. import { now } from '../mol-util/now';
  10. import { Vec3, Vec2 } from '../mol-math/linear-algebra';
  11. import { InputObserver, ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
  12. import { Renderer, RendererStats, RendererParams } from '../mol-gl/renderer';
  13. import { GraphicsRenderObject } from '../mol-gl/render-object';
  14. import { TrackballControls, TrackballControlsParams } from './controls/trackball';
  15. import { Viewport } from './camera/util';
  16. import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
  17. import { Representation } from '../mol-repr/representation';
  18. import { Scene } from '../mol-gl/scene';
  19. import { PickingId } from '../mol-geo/geometry/picking';
  20. import { MarkerAction } from '../mol-util/marker-action';
  21. import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
  22. import { Camera } from './camera';
  23. import { ParamDefinition as PD } from '../mol-util/param-definition';
  24. import { DebugHelperParams } from './helper/bounding-sphere-helper';
  25. import { SetUtils } from '../mol-util/set';
  26. import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
  27. import { PostprocessingParams } from './passes/postprocessing';
  28. import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
  29. import { PickData } from './passes/pick';
  30. import { PickHelper } from './passes/pick';
  31. import { ImagePass, ImageProps } from './passes/image';
  32. import { Sphere3D } from '../mol-math/geometry';
  33. import { addConsoleStatsProvider, isDebugMode, isTimingMode, removeConsoleStatsProvider } from '../mol-util/debug';
  34. import { CameraHelperParams } from './helper/camera-helper';
  35. import { produce } from 'immer';
  36. import { HandleHelperParams } from './helper/handle-helper';
  37. import { StereoCamera, StereoCameraParams } from './camera/stereo';
  38. import { Helper } from './helper/helper';
  39. import { Passes } from './passes/passes';
  40. import { shallowEqual } from '../mol-util';
  41. import { MarkingParams } from './passes/marking';
  42. import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit, GraphicsRenderVariantsDpoit } from '../mol-gl/webgl/render-item';
  43. import { degToRad, radToDeg } from '../mol-math/misc';
  44. import { AssetManager } from '../mol-util/assets';
  45. import { deepClone } from '../mol-util/object';
  46. export const Canvas3DParams = {
  47. camera: PD.Group({
  48. mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }),
  49. helper: PD.Group(CameraHelperParams, { isFlat: true }),
  50. stereo: PD.MappedStatic('off', {
  51. on: PD.Group(StereoCameraParams),
  52. off: PD.Group({})
  53. }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
  54. fov: PD.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }),
  55. manualReset: PD.Boolean(false, { isHidden: true }),
  56. }, { pivot: 'mode' }),
  57. cameraFog: PD.MappedStatic('on', {
  58. on: PD.Group({
  59. intensity: PD.Numeric(15, { min: 1, max: 100, step: 1 }),
  60. }),
  61. off: PD.Group({})
  62. }, { cycle: true, description: 'Show fog in the distance' }),
  63. cameraClipping: PD.Group({
  64. radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
  65. far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
  66. minNear: PD.Numeric(5, { min: 0.1, max: 100, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
  67. }, { pivot: 'radius' }),
  68. viewport: PD.MappedStatic('canvas', {
  69. canvas: PD.Group({}),
  70. 'static-frame': PD.Group({
  71. x: PD.Numeric(0),
  72. y: PD.Numeric(0),
  73. width: PD.Numeric(128),
  74. height: PD.Numeric(128)
  75. }),
  76. 'relative-frame': PD.Group({
  77. x: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
  78. y: PD.Numeric(0.33, { min: 0, max: 1, step: 0.01 }),
  79. width: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 }),
  80. height: PD.Numeric(0.5, { min: 0.01, max: 1, step: 0.01 })
  81. })
  82. }),
  83. cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
  84. sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
  85. transparentBackground: PD.Boolean(false),
  86. dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
  87. multiSample: PD.Group(MultiSampleParams),
  88. postprocessing: PD.Group(PostprocessingParams),
  89. marking: PD.Group(MarkingParams),
  90. renderer: PD.Group(RendererParams),
  91. trackball: PD.Group(TrackballControlsParams),
  92. interaction: PD.Group(Canvas3dInteractionHelperParams),
  93. debug: PD.Group(DebugHelperParams),
  94. handle: PD.Group(HandleHelperParams),
  95. };
  96. export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
  97. export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
  98. export type PartialCanvas3DProps = {
  99. [K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
  100. }
  101. export { Canvas3DContext };
  102. /** Can be used to create multiple Canvas3D objects */
  103. interface Canvas3DContext {
  104. readonly canvas?: HTMLCanvasElement
  105. readonly webgl: WebGLContext
  106. readonly input: InputObserver
  107. readonly passes: Passes
  108. readonly attribs: Readonly<Canvas3DContext.Attribs>
  109. readonly contextLost?: BehaviorSubject<now.Timestamp>
  110. readonly contextRestored?: BehaviorSubject<now.Timestamp>
  111. readonly assetManager: AssetManager
  112. dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
  113. }
  114. namespace Canvas3DContext {
  115. export const DefaultAttribs = {
  116. powerPreference: 'high-performance' as WebGLContextAttributes['powerPreference'],
  117. failIfMajorPerformanceCaveat: false,
  118. /** true by default to avoid issues with Safari (Jan 2021) */
  119. antialias: true,
  120. /** true to support multiple Canvas3D objects with a single context */
  121. preserveDrawingBuffer: true,
  122. pixelScale: 1,
  123. pickScale: 0.25,
  124. /** extra pixels to around target to check in case target is empty */
  125. pickPadding: 1,
  126. enableWboit: true,
  127. enableDpoit: false,
  128. preferWebGl1: false
  129. };
  130. export type Attribs = typeof DefaultAttribs
  131. export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext {
  132. const a = { ...DefaultAttribs, ...attribs };
  133. if (a.enableWboit && a.enableDpoit) throw new Error('Multiple transparency methods not allowed.');
  134. const { powerPreference, failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
  135. const gl = getGLContext(canvas, {
  136. powerPreference,
  137. failIfMajorPerformanceCaveat,
  138. antialias,
  139. preserveDrawingBuffer,
  140. alpha: true, // the renderer requires an alpha channel
  141. depth: true, // the renderer requires a depth buffer
  142. premultipliedAlpha: true, // the renderer outputs PMA
  143. preferWebGl1
  144. });
  145. if (gl === null) throw new Error('Could not create a WebGL rendering context');
  146. const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
  147. const webgl = createContext(gl, { pixelScale });
  148. const passes = new Passes(webgl, assetManager, a);
  149. if (isDebugMode) {
  150. const loseContextExt = gl.getExtension('WEBGL_lose_context');
  151. if (loseContextExt) {
  152. // Hold down shift+ctrl+alt and press any mouse button to call `loseContext`.
  153. // After 1 second `restoreContext` will be called.
  154. canvas.addEventListener('mousedown', e => {
  155. if (webgl.isContextLost) return;
  156. if (!e.shiftKey || !e.ctrlKey || !e.altKey) return;
  157. if (isDebugMode) console.log('lose context');
  158. loseContextExt.loseContext();
  159. setTimeout(() => {
  160. if (!webgl.isContextLost) return;
  161. if (isDebugMode) console.log('restore context');
  162. loseContextExt.restoreContext();
  163. }, 1000);
  164. }, false);
  165. }
  166. }
  167. // https://www.khronos.org/webgl/wiki/HandlingContextLost
  168. const contextLost = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  169. const handleWebglContextLost = (e: Event) => {
  170. webgl.setContextLost();
  171. e.preventDefault();
  172. if (isDebugMode) console.log('context lost');
  173. contextLost.next(now());
  174. };
  175. const handlewWebglContextRestored = () => {
  176. if (!webgl.isContextLost) return;
  177. webgl.handleContextRestored(() => {
  178. passes.draw.reset();
  179. });
  180. if (isDebugMode) console.log('context restored');
  181. };
  182. canvas.addEventListener('webglcontextlost', handleWebglContextLost, false);
  183. canvas.addEventListener('webglcontextrestored', handlewWebglContextRestored, false);
  184. return {
  185. canvas,
  186. webgl,
  187. input,
  188. passes,
  189. attribs: a,
  190. contextLost,
  191. contextRestored: webgl.contextRestored,
  192. assetManager,
  193. dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
  194. input.dispose();
  195. canvas.removeEventListener('webglcontextlost', handleWebglContextLost, false);
  196. canvas.removeEventListener('webglcontextrestored', handlewWebglContextRestored, false);
  197. webgl.destroy(options);
  198. }
  199. };
  200. }
  201. }
  202. export { Canvas3D };
  203. interface Canvas3D {
  204. readonly webgl: WebGLContext,
  205. add(repr: Representation.Any): void
  206. remove(repr: Representation.Any): void
  207. /**
  208. * This function must be called if animate() is not set up so that add/remove actions take place.
  209. */
  210. commit(isSynchronous?: boolean): void
  211. /**
  212. * Function for external "animation" control
  213. * Calls commit.
  214. */
  215. tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
  216. update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
  217. clear(): void
  218. syncVisibility(): void
  219. requestDraw(): void
  220. /** Reset the timers, used by "animate" */
  221. resetTime(t: number): void
  222. animate(): void
  223. /**
  224. * Pause animation loop and optionally any rendering
  225. * @param noDraw pause any rendering (drawPaused = true)
  226. */
  227. pause(noDraw?: boolean): void
  228. /** Sets drawPaused = false without starting the built in animation loop */
  229. resume(): void
  230. identify(x: number, y: number): PickData | undefined
  231. mark(loci: Representation.Loci, action: MarkerAction): void
  232. getLoci(pickingId: PickingId | undefined): Representation.Loci
  233. notifyDidDraw: boolean,
  234. readonly didDraw: BehaviorSubject<now.Timestamp>
  235. readonly commited: BehaviorSubject<now.Timestamp>
  236. readonly commitQueueSize: BehaviorSubject<number>
  237. readonly reprCount: BehaviorSubject<number>
  238. readonly resized: BehaviorSubject<any>
  239. handleResize(): void
  240. /** performs handleResize on the next animation frame */
  241. requestResize(): void
  242. /** Focuses camera on scene's bounding sphere, centered and zoomed. */
  243. requestCameraReset(options?: { durationMs?: number, snapshot?: Camera.SnapshotProvider }): void
  244. readonly camera: Camera
  245. readonly boundingSphere: Readonly<Sphere3D>
  246. readonly boundingSphereVisible: Readonly<Sphere3D>
  247. setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
  248. getImagePass(props: Partial<ImageProps>): ImagePass
  249. getRenderObjects(): GraphicsRenderObject[]
  250. /** Returns a copy of the current Canvas3D instance props */
  251. readonly props: Readonly<Canvas3DProps>
  252. readonly input: InputObserver
  253. readonly stats: RendererStats
  254. readonly interaction: Canvas3dInteractionHelper['events']
  255. dispose(): void
  256. }
  257. const requestAnimationFrame = typeof window !== 'undefined'
  258. ? window.requestAnimationFrame
  259. : (f: (time: number) => void) => setImmediate(() => f(Date.now())) as unknown as number;
  260. const cancelAnimationFrame = typeof window !== 'undefined'
  261. ? window.cancelAnimationFrame
  262. : (handle: number) => clearImmediate(handle as unknown as NodeJS.Immediate);
  263. namespace Canvas3D {
  264. export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  265. export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
  266. export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
  267. export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
  268. const p: Canvas3DProps = { ...deepClone(DefaultCanvas3DParams), ...deepClone(props) };
  269. const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
  270. const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
  271. const reprCount = new BehaviorSubject(0);
  272. let startTime = now();
  273. const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  274. const commited = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
  275. const commitQueueSize = new BehaviorSubject<number>(0);
  276. const { gl, contextRestored } = webgl;
  277. let x = 0;
  278. let y = 0;
  279. let width = 128;
  280. let height = 128;
  281. updateViewport();
  282. const scene = Scene.create(webgl, passes.draw.dpoitEnabled ? GraphicsRenderVariantsDpoit : (passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended));
  283. function getSceneRadius() {
  284. return scene.boundingSphere.radius * p.sceneRadiusFactor;
  285. }
  286. const camera = new Camera({
  287. position: Vec3.create(0, 0, 100),
  288. mode: p.camera.mode,
  289. fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
  290. clipFar: p.cameraClipping.far,
  291. minNear: p.cameraClipping.minNear,
  292. fov: degToRad(p.camera.fov),
  293. }, { x, y, width, height }, { pixelScale: attribs.pixelScale });
  294. const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
  295. const controls = TrackballControls.create(input, camera, scene, p.trackball);
  296. const renderer = Renderer.create(webgl, p.renderer);
  297. const helper = new Helper(webgl, scene, p);
  298. const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding);
  299. const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, controls, p.interaction);
  300. const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
  301. passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
  302. if (changed) requestDraw();
  303. });
  304. let cameraResetRequested = false;
  305. let nextCameraResetDuration: number | undefined = void 0;
  306. let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
  307. let resizeRequested = false;
  308. let notifyDidDraw = true;
  309. function getLoci(pickingId: PickingId | undefined) {
  310. let loci: Loci = EmptyLoci;
  311. let repr: Representation.Any = Representation.Empty;
  312. if (pickingId) {
  313. const cameraHelperLoci = helper.camera.getLoci(pickingId);
  314. if (cameraHelperLoci !== EmptyLoci) return { loci: cameraHelperLoci, repr };
  315. loci = helper.handle.getLoci(pickingId);
  316. reprRenderObjects.forEach((_, _repr) => {
  317. const _loci = _repr.getLoci(pickingId);
  318. if (!isEmptyLoci(_loci)) {
  319. if (!isEmptyLoci(loci)) {
  320. console.warn('found another loci, this should not happen');
  321. }
  322. loci = _loci;
  323. repr = _repr;
  324. }
  325. });
  326. }
  327. return { loci, repr };
  328. }
  329. let markBuffer: [reprLoci: Representation.Loci, action: MarkerAction][] = [];
  330. function mark(reprLoci: Representation.Loci, action: MarkerAction) {
  331. // NOTE: might try to optimize a case with opposite actions for the
  332. // same loci. Tho this might end up being more expensive (and error prone)
  333. // then just applying everything "naively".
  334. markBuffer.push([reprLoci, action]);
  335. }
  336. function resolveMarking() {
  337. let changed = false;
  338. for (const [r, l] of markBuffer) {
  339. changed = applyMark(r, l) || changed;
  340. }
  341. markBuffer = [];
  342. if (changed) {
  343. scene.update(void 0, true);
  344. helper.handle.scene.update(void 0, true);
  345. helper.camera.scene.update(void 0, true);
  346. }
  347. return changed;
  348. }
  349. function applyMark(reprLoci: Representation.Loci, action: MarkerAction) {
  350. const { repr, loci } = reprLoci;
  351. let changed = false;
  352. if (repr) {
  353. changed = repr.mark(loci, action) || changed;
  354. } else {
  355. reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
  356. }
  357. changed = helper.handle.mark(loci, action) || changed;
  358. changed = helper.camera.mark(loci, action) || changed;
  359. return changed;
  360. }
  361. function render(force: boolean) {
  362. if (webgl.isContextLost) return false;
  363. let resized = false;
  364. if (resizeRequested) {
  365. handleResize(false);
  366. resizeRequested = false;
  367. resized = true;
  368. }
  369. if (x > gl.drawingBufferWidth || x + width < 0 ||
  370. y > gl.drawingBufferHeight || y + height < 0
  371. ) return false;
  372. const markingUpdated = resolveMarking() && (renderer.props.colorMarker || p.marking.enabled);
  373. let didRender = false;
  374. controls.update(currentTime);
  375. const cameraChanged = camera.update();
  376. const shouldRender = force || cameraChanged || resized || forceNextRender;
  377. forceNextRender = false;
  378. const multiSampleChanged = multiSampleHelper.update(markingUpdated || shouldRender, p.multiSample);
  379. if (shouldRender || multiSampleChanged || markingUpdated) {
  380. let cam: Camera | StereoCamera = camera;
  381. if (p.camera.stereo.name === 'on') {
  382. stereoCamera.update();
  383. cam = stereoCamera;
  384. }
  385. if (isTimingMode) webgl.timer.mark('Canvas3D.render', true);
  386. const ctx = { renderer, camera: cam, scene, helper };
  387. if (MultiSamplePass.isEnabled(p.multiSample)) {
  388. const forceOn = p.multiSample.reduceFlicker && !cameraChanged && markingUpdated && !controls.isAnimating;
  389. multiSampleHelper.render(ctx, p, true, forceOn);
  390. } else {
  391. passes.draw.render(ctx, p, true);
  392. }
  393. if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
  394. // if only marking has updated, do not set the flag to dirty
  395. pickHelper.dirty = pickHelper.dirty || shouldRender;
  396. didRender = true;
  397. }
  398. return didRender;
  399. }
  400. let forceNextRender = false;
  401. let forceDrawAfterAllCommited = false;
  402. let currentTime = 0;
  403. let drawPaused = false;
  404. function draw(options?: { force?: boolean }) {
  405. if (drawPaused) return;
  406. if (render(!!options?.force) && notifyDidDraw) {
  407. didDraw.next(now() - startTime as now.Timestamp);
  408. }
  409. }
  410. function requestDraw() {
  411. forceNextRender = true;
  412. }
  413. let animationFrameHandle = 0;
  414. function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
  415. currentTime = t;
  416. commit(options?.isSynchronous);
  417. camera.transition.tick(currentTime);
  418. if (options?.manualDraw) {
  419. return;
  420. }
  421. draw();
  422. if (!camera.transition.inTransition && !webgl.isContextLost) {
  423. interactionHelper.tick(currentTime);
  424. }
  425. }
  426. function _animate() {
  427. tick(now());
  428. animationFrameHandle = requestAnimationFrame(_animate);
  429. }
  430. function resetTime(t: now.Timestamp) {
  431. startTime = t;
  432. controls.start(t);
  433. }
  434. function animate() {
  435. drawPaused = false;
  436. controls.start(now());
  437. if (animationFrameHandle === 0) _animate();
  438. }
  439. function pause(noDraw = false) {
  440. drawPaused = noDraw;
  441. cancelAnimationFrame(animationFrameHandle);
  442. animationFrameHandle = 0;
  443. }
  444. function identify(x: number, y: number): PickData | undefined {
  445. const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
  446. return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
  447. }
  448. function commit(isSynchronous: boolean = false) {
  449. const allCommited = commitScene(isSynchronous);
  450. // Only reset the camera after the full scene has been commited.
  451. if (allCommited) {
  452. resolveCameraReset();
  453. if (forceDrawAfterAllCommited) {
  454. if (helper.debug.isEnabled) helper.debug.update();
  455. draw({ force: true });
  456. forceDrawAfterAllCommited = false;
  457. }
  458. commited.next(now());
  459. }
  460. }
  461. function resolveCameraReset() {
  462. if (!cameraResetRequested) return;
  463. const boundingSphere = scene.boundingSphereVisible;
  464. const { center, radius } = boundingSphere;
  465. const autoAdjustControls = controls.props.autoAdjustMinMaxDistance;
  466. if (autoAdjustControls.name === 'on') {
  467. const minDistance = autoAdjustControls.params.minDistanceFactor * radius + autoAdjustControls.params.minDistancePadding;
  468. const maxDistance = Math.max(autoAdjustControls.params.maxDistanceFactor * radius, autoAdjustControls.params.maxDistanceMin);
  469. controls.setProps({ minDistance, maxDistance });
  470. }
  471. if (radius > 0) {
  472. const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration;
  473. const focus = camera.getFocus(center, radius);
  474. const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
  475. const snapshot = next ? { ...focus, ...next } : focus;
  476. camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration);
  477. }
  478. nextCameraResetDuration = void 0;
  479. nextCameraResetSnapshot = void 0;
  480. cameraResetRequested = false;
  481. }
  482. const oldBoundingSphereVisible = Sphere3D();
  483. const cameraSphere = Sphere3D();
  484. function shouldResetCamera() {
  485. if (camera.state.radiusMax === 0) return true;
  486. if (camera.transition.inTransition || nextCameraResetSnapshot) return false;
  487. let cameraSphereOverlapsNone = true, isEmpty = true;
  488. Sphere3D.set(cameraSphere, camera.state.target, camera.state.radius);
  489. // check if any renderable has moved outside of the old bounding sphere
  490. // and if no renderable is overlapping with the camera sphere
  491. for (const r of scene.renderables) {
  492. if (!r.state.visible) continue;
  493. const b = r.values.boundingSphere.ref.value;
  494. if (!b.radius) continue;
  495. isEmpty = false;
  496. const cameraDist = Vec3.distance(cameraSphere.center, b.center);
  497. if ((cameraDist > cameraSphere.radius || cameraDist > b.radius || b.radius > camera.state.radiusMax) && !Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
  498. if (Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false;
  499. }
  500. return cameraSphereOverlapsNone || (!isEmpty && cameraSphere.radius <= 0.1);
  501. }
  502. const sceneCommitTimeoutMs = 250;
  503. function commitScene(isSynchronous: boolean) {
  504. if (!scene.needsCommit) return true;
  505. // snapshot the current bounding sphere of visible objects
  506. Sphere3D.copy(oldBoundingSphereVisible, scene.boundingSphereVisible);
  507. if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) {
  508. commitQueueSize.next(scene.commitQueueSize);
  509. return false;
  510. }
  511. commitQueueSize.next(0);
  512. if (helper.debug.isEnabled) helper.debug.update();
  513. if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
  514. cameraResetRequested = true;
  515. }
  516. if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
  517. if (!p.camera.manualReset) camera.setState({ radiusMax: getSceneRadius() }, 0);
  518. reprCount.next(reprRenderObjects.size);
  519. if (isDebugMode) consoleStats();
  520. return true;
  521. }
  522. function consoleStats() {
  523. const items = scene.renderables.map(r => ({
  524. drawCount: r.values.drawCount.ref.value,
  525. instanceCount: r.values.instanceCount.ref.value,
  526. materialId: r.materialId,
  527. renderItemId: r.id,
  528. }));
  529. console.groupCollapsed(`${items.length} RenderItems`);
  530. if (items.length < 50) {
  531. console.table(items);
  532. } else {
  533. console.log(items);
  534. }
  535. console.log(JSON.stringify(webgl.stats, undefined, 4));
  536. const { texture, attribute, elements } = webgl.resources.getByteCounts();
  537. console.log(JSON.stringify({
  538. texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
  539. attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
  540. elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
  541. }, undefined, 4));
  542. console.log(JSON.stringify(webgl.timer.formatedStats(), undefined, 4));
  543. console.groupEnd();
  544. }
  545. function add(repr: Representation.Any) {
  546. registerAutoUpdate(repr);
  547. const oldRO = reprRenderObjects.get(repr);
  548. const newRO = new Set<GraphicsRenderObject>();
  549. repr.renderObjects.forEach(o => newRO.add(o));
  550. if (oldRO) {
  551. if (!SetUtils.areEqual(newRO, oldRO)) {
  552. newRO.forEach(o => { if (!oldRO.has(o)) scene.add(o); });
  553. oldRO.forEach(o => { if (!newRO.has(o)) scene.remove(o); });
  554. }
  555. } else {
  556. repr.renderObjects.forEach(o => scene.add(o));
  557. }
  558. reprRenderObjects.set(repr, newRO);
  559. scene.update(repr.renderObjects, false);
  560. forceDrawAfterAllCommited = true;
  561. if (isDebugMode) consoleStats();
  562. }
  563. function remove(repr: Representation.Any) {
  564. unregisterAutoUpdate(repr);
  565. const renderObjects = reprRenderObjects.get(repr);
  566. if (renderObjects) {
  567. renderObjects.forEach(o => scene.remove(o));
  568. reprRenderObjects.delete(repr);
  569. forceDrawAfterAllCommited = true;
  570. if (isDebugMode) consoleStats();
  571. }
  572. }
  573. function registerAutoUpdate(repr: Representation.Any) {
  574. if (reprUpdatedSubscriptions.has(repr)) return;
  575. reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => {
  576. if (!repr.state.syncManually) add(repr);
  577. }));
  578. }
  579. function unregisterAutoUpdate(repr: Representation.Any) {
  580. const updatedSubscription = reprUpdatedSubscriptions.get(repr);
  581. if (updatedSubscription) {
  582. updatedSubscription.unsubscribe();
  583. reprUpdatedSubscriptions.delete(repr);
  584. }
  585. }
  586. function getProps(): Canvas3DProps {
  587. const radius = scene.boundingSphere.radius > 0
  588. ? 100 - Math.round((camera.transition.target.radius / getSceneRadius()) * 100)
  589. : 0;
  590. return {
  591. camera: {
  592. mode: camera.state.mode,
  593. helper: { ...helper.camera.props },
  594. stereo: { ...p.camera.stereo },
  595. fov: Math.round(radToDeg(camera.state.fov)),
  596. manualReset: !!p.camera.manualReset
  597. },
  598. cameraFog: camera.state.fog > 0
  599. ? { name: 'on' as const, params: { intensity: camera.state.fog } }
  600. : { name: 'off' as const, params: {} },
  601. cameraClipping: { far: camera.state.clipFar, radius, minNear: camera.state.minNear },
  602. cameraResetDurationMs: p.cameraResetDurationMs,
  603. sceneRadiusFactor: p.sceneRadiusFactor,
  604. transparentBackground: p.transparentBackground,
  605. dpoitIterations: p.dpoitIterations,
  606. viewport: p.viewport,
  607. postprocessing: { ...p.postprocessing },
  608. marking: { ...p.marking },
  609. multiSample: { ...p.multiSample },
  610. renderer: { ...renderer.props },
  611. trackball: { ...controls.props },
  612. interaction: { ...interactionHelper.props },
  613. debug: { ...helper.debug.props },
  614. handle: { ...helper.handle.props },
  615. };
  616. }
  617. const contextRestoredSub = contextRestored.subscribe(() => {
  618. pickHelper.dirty = true;
  619. draw({ force: true });
  620. // Unclear why, but in Chrome with wboit enabled the first `draw` only clears
  621. // the drawingBuffer. Note that in Firefox the drawingBuffer is preserved after
  622. // context loss so it is unclear if it behaves the same.
  623. draw({ force: true });
  624. });
  625. const resized = new BehaviorSubject<any>(0);
  626. function handleResize(draw = true) {
  627. passes.updateSize();
  628. updateViewport();
  629. syncViewport();
  630. if (draw) requestDraw();
  631. resized.next(+new Date());
  632. }
  633. addConsoleStatsProvider(consoleStats);
  634. return {
  635. webgl,
  636. add,
  637. remove,
  638. commit,
  639. update: (repr, keepSphere) => {
  640. if (repr) {
  641. if (!reprRenderObjects.has(repr)) return;
  642. scene.update(repr.renderObjects, !!keepSphere);
  643. } else {
  644. scene.update(void 0, !!keepSphere);
  645. }
  646. forceDrawAfterAllCommited = true;
  647. },
  648. clear: () => {
  649. reprUpdatedSubscriptions.forEach(v => v.unsubscribe());
  650. reprUpdatedSubscriptions.clear();
  651. reprRenderObjects.clear();
  652. scene.clear();
  653. helper.debug.clear();
  654. requestDraw();
  655. reprCount.next(reprRenderObjects.size);
  656. },
  657. syncVisibility: () => {
  658. if (camera.state.radiusMax === 0) {
  659. cameraResetRequested = true;
  660. nextCameraResetDuration = 0;
  661. }
  662. if (scene.syncVisibility()) {
  663. if (helper.debug.isEnabled) helper.debug.update();
  664. }
  665. requestDraw();
  666. },
  667. requestDraw,
  668. tick,
  669. animate,
  670. resetTime,
  671. pause,
  672. resume: () => { drawPaused = false; },
  673. identify,
  674. mark,
  675. getLoci,
  676. handleResize,
  677. requestResize: () => {
  678. resizeRequested = true;
  679. },
  680. requestCameraReset: options => {
  681. nextCameraResetDuration = options?.durationMs;
  682. nextCameraResetSnapshot = options?.snapshot;
  683. cameraResetRequested = true;
  684. },
  685. camera,
  686. boundingSphere: scene.boundingSphere,
  687. boundingSphereVisible: scene.boundingSphereVisible,
  688. get notifyDidDraw() { return notifyDidDraw; },
  689. set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
  690. didDraw,
  691. commited,
  692. commitQueueSize,
  693. reprCount,
  694. resized,
  695. setProps: (properties, doNotRequestDraw = false) => {
  696. const props: PartialCanvas3DProps = typeof properties === 'function'
  697. ? produce(getProps(), properties as any)
  698. : properties;
  699. if (props.sceneRadiusFactor !== undefined) {
  700. p.sceneRadiusFactor = props.sceneRadiusFactor;
  701. camera.setState({ radiusMax: getSceneRadius() }, 0);
  702. }
  703. const cameraState: Partial<Camera.Snapshot> = Object.create(null);
  704. if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
  705. cameraState.mode = props.camera.mode;
  706. }
  707. const oldFov = Math.round(radToDeg(camera.state.fov));
  708. if (props.camera && props.camera.fov !== undefined && props.camera.fov !== oldFov) {
  709. cameraState.fov = degToRad(props.camera.fov);
  710. }
  711. if (props.cameraFog !== undefined && props.cameraFog.params) {
  712. const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
  713. if (newFog !== camera.state.fog) cameraState.fog = newFog;
  714. }
  715. if (props.cameraClipping !== undefined) {
  716. if (props.cameraClipping.far !== undefined && props.cameraClipping.far !== camera.state.clipFar) {
  717. cameraState.clipFar = props.cameraClipping.far;
  718. }
  719. if (props.cameraClipping.minNear !== undefined && props.cameraClipping.minNear !== camera.state.minNear) {
  720. cameraState.minNear = props.cameraClipping.minNear;
  721. }
  722. if (props.cameraClipping.radius !== undefined) {
  723. const radius = (getSceneRadius() / 100) * (100 - props.cameraClipping.radius);
  724. if (radius > 0 && radius !== cameraState.radius) {
  725. // if radius = 0, NaNs happen
  726. cameraState.radius = Math.max(radius, 0.01);
  727. }
  728. }
  729. }
  730. if (Object.keys(cameraState).length > 0) camera.setState(cameraState);
  731. if (props.camera?.helper) helper.camera.setProps(props.camera.helper);
  732. if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset;
  733. if (props.camera?.stereo !== undefined) {
  734. Object.assign(p.camera.stereo, props.camera.stereo);
  735. stereoCamera.setProps(p.camera.stereo.params);
  736. }
  737. if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
  738. if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
  739. if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
  740. if (props.viewport !== undefined) {
  741. const doNotUpdate = p.viewport === props.viewport ||
  742. (p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
  743. if (!doNotUpdate) {
  744. p.viewport = props.viewport;
  745. updateViewport();
  746. syncViewport();
  747. }
  748. }
  749. if (props.postprocessing?.background) {
  750. Object.assign(p.postprocessing.background, props.postprocessing.background);
  751. passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
  752. if (changed && !doNotRequestDraw) requestDraw();
  753. });
  754. }
  755. if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
  756. if (props.marking) Object.assign(p.marking, props.marking);
  757. if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
  758. if (props.renderer) renderer.setProps(props.renderer);
  759. if (props.trackball) controls.setProps(props.trackball);
  760. if (props.interaction) interactionHelper.setProps(props.interaction);
  761. if (props.debug) helper.debug.setProps(props.debug);
  762. if (props.handle) helper.handle.setProps(props.handle);
  763. if (cameraState.mode === 'orthographic') {
  764. p.camera.stereo.name = 'off';
  765. }
  766. if (!doNotRequestDraw) {
  767. requestDraw();
  768. }
  769. },
  770. getImagePass: (props: Partial<ImageProps> = {}) => {
  771. return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, passes.draw.wboitEnabled, passes.draw.dpoitEnabled, props);
  772. },
  773. getRenderObjects(): GraphicsRenderObject[] {
  774. const renderObjects: GraphicsRenderObject[] = [];
  775. scene.forEach((_, ro) => renderObjects.push(ro));
  776. return renderObjects;
  777. },
  778. get props() {
  779. return getProps();
  780. },
  781. get input() {
  782. return input;
  783. },
  784. get stats() {
  785. return renderer.stats;
  786. },
  787. get interaction() {
  788. return interactionHelper.events;
  789. },
  790. dispose: () => {
  791. contextRestoredSub.unsubscribe();
  792. cancelAnimationFrame(animationFrameHandle);
  793. markBuffer = [];
  794. scene.clear();
  795. helper.debug.clear();
  796. controls.dispose();
  797. renderer.dispose();
  798. interactionHelper.dispose();
  799. removeConsoleStatsProvider(consoleStats);
  800. }
  801. };
  802. function updateViewport() {
  803. const oldX = x, oldY = y, oldWidth = width, oldHeight = height;
  804. if (p.viewport.name === 'canvas') {
  805. x = 0;
  806. y = 0;
  807. width = gl.drawingBufferWidth;
  808. height = gl.drawingBufferHeight;
  809. } else if (p.viewport.name === 'static-frame') {
  810. x = p.viewport.params.x * webgl.pixelRatio;
  811. height = p.viewport.params.height * webgl.pixelRatio;
  812. y = gl.drawingBufferHeight - height - p.viewport.params.y * webgl.pixelRatio;
  813. width = p.viewport.params.width * webgl.pixelRatio;
  814. } else if (p.viewport.name === 'relative-frame') {
  815. x = Math.round(p.viewport.params.x * gl.drawingBufferWidth);
  816. height = Math.round(p.viewport.params.height * gl.drawingBufferHeight);
  817. y = Math.round(gl.drawingBufferHeight - height - p.viewport.params.y * gl.drawingBufferHeight);
  818. width = Math.round(p.viewport.params.width * gl.drawingBufferWidth);
  819. }
  820. if (oldX !== x || oldY !== y || oldWidth !== width || oldHeight !== height) {
  821. forceNextRender = true;
  822. }
  823. }
  824. function syncViewport() {
  825. pickHelper.setViewport(x, y, width, height);
  826. renderer.setViewport(x, y, width, height);
  827. Viewport.set(camera.viewport, x, y, width, height);
  828. Viewport.set(controls.viewport, x, y, width, height);
  829. }
  830. }
  831. }