123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407 |
- /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
- import { BehaviorSubject, Subscription } from 'rxjs';
- import { now } from 'mol-util/now';
- import { Vec3 } from 'mol-math/linear-algebra'
- import InputObserver from 'mol-util/input/input-observer'
- import * as SetUtils from 'mol-util/set'
- import Renderer, { RendererStats } from 'mol-gl/renderer'
- import { RenderObject } from 'mol-gl/render-object'
- import TrackballControls from './controls/trackball'
- import { Viewport } from './camera/util'
- import { resizeCanvas } from './util';
- import { createContext, getGLContext, WebGLContext } from 'mol-gl/webgl/context';
- import { Representation } from 'mol-repr/representation';
- import { createRenderTarget } from 'mol-gl/webgl/render-target';
- import Scene from 'mol-gl/scene';
- import { RenderVariant } from 'mol-gl/webgl/render-item';
- import { PickingId, decodeIdRGB } from 'mol-geo/geometry/picking';
- import { MarkerAction } from 'mol-geo/geometry/marker-data';
- import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
- import { Color } from 'mol-util/color';
- import { Camera } from './camera';
- import { ParamDefinition as PD } from 'mol-util/param-definition';
- export const Canvas3DParams = {
- // TODO: FPS cap?
- // maxFps: PD.Numeric(30),
- cameraPosition: PD.Vec3(Vec3.create(0, 0, 50)), // TODO or should it be in a seperate 'state' property?
- cameraMode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']]),
- backgroundColor: PD.Color(Color(0x000000)),
- 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.' }),
- }
- export type Canvas3DParams = typeof Canvas3DParams
- export { Canvas3D }
- interface Canvas3D {
- readonly webgl: WebGLContext,
- hide: (repr: Representation.Any) => void
- show: (repr: Representation.Any) => void
- add: (repr: Representation.Any) => void
- remove: (repr: Representation.Any) => void
- update: () => void
- clear: () => void
- // draw: (force?: boolean) => void
- requestDraw: (force?: boolean) => void
- animate: () => void
- pick: () => void
- identify: (x: number, y: number) => Promise<PickingId | undefined>
- mark: (loci: Loci, action: MarkerAction, repr?: Representation.Any) => void
- getLoci: (pickingId: PickingId) => { loci: Loci, repr?: Representation.Any }
- readonly didDraw: BehaviorSubject<now.Timestamp>
- handleResize: () => void
- resetCamera: () => void
- readonly camera: Camera
- downloadScreenshot: () => void
- getImageData: (variant: RenderVariant) => ImageData
- setProps: (props: Partial<PD.Values<Canvas3DParams>>) => void
- /** Returns a copy of the current Canvas3D instance props */
- readonly props: PD.Values<Canvas3DParams>
- readonly input: InputObserver
- readonly stats: RendererStats
- dispose: () => void
- }
- namespace Canvas3D {
- export function create(canvas: HTMLCanvasElement, container: Element, props: Partial<PD.Values<Canvas3DParams>> = {}): Canvas3D {
- const p = { ...PD.getDefaultValues(Canvas3DParams), ...props }
- const reprRenderObjects = new Map<Representation.Any, Set<RenderObject>>()
- const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
- const reprCount = new BehaviorSubject(0)
- const startTime = now()
- const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp)
- const input = InputObserver.create(canvas)
- const camera = new Camera({
- near: 0.1,
- far: 10000,
- position: Vec3.clone(p.cameraPosition),
- mode: p.cameraMode
- })
- const gl = getGLContext(canvas, {
- alpha: false,
- antialias: true,
- depth: true,
- preserveDrawingBuffer: true
- })
- if (gl === null) {
- throw new Error('Could not create a WebGL rendering context')
- }
- const webgl = createContext(gl)
- const scene = Scene.create(webgl)
- const controls = TrackballControls.create(input, camera, {})
- const renderer = Renderer.create(webgl, camera, { clearColor: p.backgroundColor })
- const pickScale = 1
- const pickWidth = Math.round(canvas.width * pickScale)
- const pickHeight = Math.round(canvas.height * pickScale)
- const objectPickTarget = createRenderTarget(webgl, pickWidth, pickHeight)
- const instancePickTarget = createRenderTarget(webgl, pickWidth, pickHeight)
- const groupPickTarget = createRenderTarget(webgl, pickWidth, pickHeight)
- let pickDirty = true
- let isPicking = false
- let drawPending = false
- let lastRenderTime = -1
- function getLoci(pickingId: PickingId) {
- let loci: Loci = EmptyLoci
- let repr: Representation.Any = Representation.Empty
- reprRenderObjects.forEach((_, _repr) => {
- const _loci = _repr.getLoci(pickingId)
- if (!isEmptyLoci(_loci)) {
- if (!isEmptyLoci(loci)) console.warn('found another loci')
- loci = _loci
- repr = _repr
- }
- })
- return { loci, repr }
- }
- function mark(loci: Loci, action: MarkerAction, repr?: Representation.Any) {
- let changed = false
- reprRenderObjects.forEach((_, _repr) => {
- if (!repr || repr === _repr) {
- changed = _repr.mark(loci, action) || changed
- }
- })
- if (changed) {
- // console.log('changed')
- scene.update()
- draw(true)
- pickDirty = false // picking buffers should not have changed
- }
- }
- // let nearPlaneDelta = 0
- // function computeNearDistance() {
- // const focusRadius = scene.boundingSphere.radius
- // let dist = Vec3.distance(controls.target, camera.position)
- // if (dist > focusRadius) return dist - focusRadius
- // return 0
- // }
- function render(variant: RenderVariant, force: boolean) {
- if (isPicking) return false
- // const p = scene.boundingSphere.center
- // console.log(p[0], p[1], p[2])
- // Vec3.set(controls.target, p[0], p[1], p[2])
- // TODO update near/far
- // const focusRadius = scene.boundingSphere.radius
- // const targetDistance = Vec3.distance(controls.target, camera.position)
- // console.log(targetDistance, controls.target, camera.position)
- // let near = computeNearDistance() + nearPlaneDelta
- // camera.near = Math.max(0.01, Math.min(near, targetDistance - 0.5))
- // let fogNear = targetDistance - camera.near + 1 * focusRadius - nearPlaneDelta;
- // let fogFar = targetDistance - camera.near + 2 * focusRadius - nearPlaneDelta;
- // // console.log(fogNear, fogFar);
- // camera.fogNear = Math.max(fogNear, 0.1);
- // camera.fogFar = Math.max(fogFar, 0.2);
- // console.log(camera.fogNear, camera.fogFar, targetDistance)
- let didRender = false
- controls.update()
- const cameraChanged = camera.updateMatrices();
- if (force || cameraChanged) {
- switch (variant) {
- case 'pickObject': objectPickTarget.bind(); break;
- case 'pickInstance': instancePickTarget.bind(); break;
- case 'pickGroup': groupPickTarget.bind(); break;
- case 'draw':
- webgl.unbindFramebuffer();
- renderer.setViewport(0, 0, canvas.width, canvas.height);
- break;
- }
- renderer.render(scene, variant)
- if (variant === 'draw') {
- lastRenderTime = now()
- pickDirty = true
- }
- didRender = true
- }
- return didRender && cameraChanged;
- }
- let forceNextDraw = false;
- function draw(force?: boolean) {
- if (render('draw', !!force || forceNextDraw)) {
- didDraw.next(now() - startTime as now.Timestamp)
- }
- forceNextDraw = false;
- drawPending = false
- }
- function requestDraw(force?: boolean) {
- if (drawPending) return
- drawPending = true
- forceNextDraw = !!force;
- // The animation frame is being requested by animate already.
- // window.requestAnimationFrame(() => draw(force))
- }
- function animate() {
- const t = now();
- camera.transition.tick(t);
- draw(false)
- if (t - lastRenderTime > 200) {
- if (pickDirty) pick()
- }
- window.requestAnimationFrame(animate)
- }
- function pick() {
- render('pickObject', pickDirty)
- render('pickInstance', pickDirty)
- render('pickGroup', pickDirty)
- webgl.gl.finish()
- pickDirty = false
- }
- async function identify(x: number, y: number): Promise<PickingId | undefined> {
- if (pickDirty || isPicking) return undefined
- isPicking = true
- x *= webgl.pixelRatio
- y *= webgl.pixelRatio
- y = canvas.height - y // flip y
- const buffer = new Uint8Array(4)
- const xp = Math.round(x * pickScale)
- const yp = Math.round(y * pickScale)
- objectPickTarget.bind()
- await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
- const objectId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
- if (objectId === -1) return
- instancePickTarget.bind()
- await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
- const instanceId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
- if (instanceId === -1) return
- groupPickTarget.bind()
- await webgl.readPixelsAsync(xp, yp, 1, 1, buffer)
- const groupId = decodeIdRGB(buffer[0], buffer[1], buffer[2])
- if (groupId === -1) return
- isPicking = false
- return { objectId, instanceId, groupId }
- }
- function add(repr: Representation.Any) {
- const oldRO = reprRenderObjects.get(repr)
- const newRO = new Set<RenderObject>()
- repr.renderObjects.forEach(o => newRO.add(o))
- if (oldRO) {
- SetUtils.difference(newRO, oldRO).forEach(o => scene.add(o))
- SetUtils.difference(oldRO, newRO).forEach(o => scene.remove(o))
- scene.update()
- } else {
- repr.renderObjects.forEach(o => scene.add(o))
- }
- reprRenderObjects.set(repr, newRO)
- reprCount.next(reprRenderObjects.size)
- scene.update()
- requestDraw(true)
- }
- handleResize()
- return {
- webgl,
- hide: (repr: Representation.Any) => {
- const renderObjectSet = reprRenderObjects.get(repr)
- if (renderObjectSet) renderObjectSet.forEach(o => o.state.visible = false)
- },
- show: (repr: Representation.Any) => {
- const renderObjectSet = reprRenderObjects.get(repr)
- if (renderObjectSet) renderObjectSet.forEach(o => o.state.visible = true)
- },
- add: (repr: Representation.Any) => {
- add(repr)
- reprUpdatedSubscriptions.set(repr, repr.updated.subscribe(_ => add(repr)))
- },
- remove: (repr: Representation.Any) => {
- const updatedSubscription = reprUpdatedSubscriptions.get(repr)
- if (updatedSubscription) {
- updatedSubscription.unsubscribe()
- }
- const renderObjects = reprRenderObjects.get(repr)
- if (renderObjects) {
- renderObjects.forEach(o => scene.remove(o))
- reprRenderObjects.delete(repr)
- reprCount.next(reprRenderObjects.size)
- scene.update()
- }
- },
- update: () => scene.update(),
- clear: () => {
- reprRenderObjects.clear()
- scene.clear()
- },
- // draw,
- requestDraw,
- animate,
- pick,
- identify,
- mark,
- getLoci,
- handleResize,
- resetCamera: () => {
- // TODO
- },
- camera,
- downloadScreenshot: () => {
- // TODO
- },
- getImageData: (variant: RenderVariant) => {
- switch (variant) {
- case 'draw': return renderer.getImageData()
- case 'pickObject': return objectPickTarget.getImageData()
- case 'pickInstance': return instancePickTarget.getImageData()
- case 'pickGroup': return groupPickTarget.getImageData()
- }
- },
- didDraw,
- setProps: (props: Partial<PD.Values<Canvas3DParams>>) => {
- if (props.cameraMode !== undefined && props.cameraMode !== camera.state.mode) {
- camera.setState({ mode: props.cameraMode })
- }
- if (props.backgroundColor !== undefined && props.backgroundColor !== renderer.props.clearColor) {
- renderer.setClearColor(props.backgroundColor)
- }
- if (props.pickingAlphaThreshold !== undefined && props.pickingAlphaThreshold !== renderer.props.pickingAlphaThreshold) {
- renderer.setPickingAlphaThreshold(props.pickingAlphaThreshold)
- }
- requestDraw(true)
- },
- get props() {
- return {
- cameraPosition: Vec3.clone(camera.position),
- cameraMode: camera.state.mode,
- backgroundColor: renderer.props.clearColor,
- pickingAlphaThreshold: renderer.props.pickingAlphaThreshold,
- }
- },
- get input() {
- return input
- },
- get stats() {
- return renderer.stats
- },
- dispose: () => {
- scene.clear()
- input.dispose()
- controls.dispose()
- renderer.dispose()
- camera.dispose()
- }
- }
- function handleResize() {
- resizeCanvas(canvas, container)
- renderer.setViewport(0, 0, canvas.width, canvas.height)
- Viewport.set(camera.viewport, 0, 0, canvas.width, canvas.height)
- Viewport.set(controls.viewport, 0, 0, canvas.width, canvas.height)
- const pickWidth = Math.round(canvas.width * pickScale)
- const pickHeight = Math.round(canvas.height * pickScale)
- objectPickTarget.setSize(pickWidth, pickHeight)
- instancePickTarget.setSize(pickWidth, pickHeight)
- groupPickTarget.setSize(pickWidth, pickHeight)
- }
- }
- }
|