diff --git a/src/apps/viewer/index.ts b/src/apps/viewer/index.ts index 3b09acdd91713ade71915338156e0f442b54c480..6cacc6c45dac530710816dcfd783eb154eb87139 100644 --- a/src/apps/viewer/index.ts +++ b/src/apps/viewer/index.ts @@ -254,6 +254,10 @@ export class Viewer { await repr.commit(); }); } + + handleResize() { + this.plugin.layout.events.updated.next(); + } } export interface VolumeIsovalueInfo { diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts index 2a0792fd5754ea5385e4999396e03a99fc5f78e2..3b99356dbf2dd6680ca97a7b8f55919ab4618dc1 100644 --- a/src/mol-canvas3d/camera.ts +++ b/src/mol-canvas3d/camera.ts @@ -16,6 +16,8 @@ interface ICamera { readonly viewport: Viewport, readonly view: Mat4, readonly projection: Mat4, + readonly projectionView: Mat4, + readonly inverseProjectionView: Mat4, readonly state: Readonly<Camera.Snapshot>, readonly viewOffset: Camera.ViewOffset, readonly far: number, diff --git a/src/mol-canvas3d/camera/stereo.ts b/src/mol-canvas3d/camera/stereo.ts index 8aa206b48cd63794cfd139d61a85be91b1753379..956e3762753cbedef5e957472578969178cfa83c 100644 --- a/src/mol-canvas3d/camera/stereo.ts +++ b/src/mol-canvas3d/camera/stereo.ts @@ -2,21 +2,56 @@ * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. * * @author David Sehnal <david.sehnal@gmail.com> + * @author Alexander Rose <alexander.rose@weirdbyte.de> * * Adapted from three.js, The MIT License, Copyright © 2010-2020 three.js authors */ import { Mat4 } from '../../mol-math/linear-algebra'; -import { ParamDefinition } from '../../mol-util/param-definition'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { Camera, ICamera } from '../camera'; import { Viewport } from './util'; -export class StereoCamera { +export const StereoCameraParams = { + eyeSeparation: PD.Numeric(0.064, { min: 0.01, max: 0.5, step: 0.001 }), + focus: PD.Numeric(10, { min: 1, max: 100, step: 0.01 }), +}; +export const DefaultStereoCameraProps = PD.getDefaultValues(StereoCameraParams); +export type StereoCameraProps = PD.Values<typeof StereoCameraParams> + +export { StereoCamera }; + +class StereoCamera { readonly left: ICamera = new EyeCamera(); readonly right: ICamera = new EyeCamera(); - update(camera: Camera, params: StereoCameraParams) { - update(camera, params, this.left as EyeCamera, this.right as EyeCamera); + get viewport() { + return this.parent.viewport; + } + + get viewOffset() { + return this.parent.viewOffset; + } + + private props: StereoCameraProps + + constructor(private parent: Camera, props: Partial<StereoCameraProps> = {}) { + this.props = { ...DefaultStereoCameraProps, ...props }; + } + + setProps(props: Partial<StereoCameraProps>) { + Object.assign(this.props, props); + } + + update() { + this.parent.update(); + update(this.parent, this.props, this.left as EyeCamera, this.right as EyeCamera); + } +} + +namespace StereoCamera { + export function is(camera: Camera | StereoCamera): camera is StereoCamera { + return 'left' in camera && 'right' in camera; } } @@ -24,6 +59,8 @@ class EyeCamera implements ICamera { viewport = Viewport.create(0, 0, 0, 0); view = Mat4(); projection = Mat4(); + projectionView = Mat4(); + inverseProjectionView = Mat4(); state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot(); viewOffset: Readonly<Camera.ViewOffset> = Camera.ViewOffset(); far: number = 0; @@ -32,16 +69,9 @@ class EyeCamera implements ICamera { fogNear: number = 0; } -export const StereoCameraParams = { - aspect: ParamDefinition.Numeric(1, { min: 0.1, max: 3, step: 0.01 }), - eyeSeparation: ParamDefinition.Numeric(0.064, { min: 0.01, max: 0.5, step: 0.001 }), - focus: ParamDefinition.Numeric(10, { min: 1, max: 100, step: 0.01 }), -}; -export type StereoCameraParams = ParamDefinition.Values<typeof StereoCameraParams> - const eyeLeft = Mat4.identity(), eyeRight = Mat4.identity(); -function update(camera: ICamera, params: StereoCameraParams, left: EyeCamera, right: EyeCamera) { +function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right: EyeCamera) { // Copy the states Viewport.copy(left.viewport, camera.viewport); @@ -65,41 +95,47 @@ function update(camera: ICamera, params: StereoCameraParams, left: EyeCamera, ri right.fogNear = camera.fogNear; // update the view offsets - let w = (camera.viewport.width / 2) | 0; + + const w = Math.floor(camera.viewport.width / 2); + const aspect = w / camera.viewport.height; left.viewport.width = w; - right.viewport.x = w; + right.viewport.x += w; right.viewport.width -= w; // update the projection and view matrices - const eyeSepHalf = params.eyeSeparation / 2; - const eyeSepOnProjection = eyeSepHalf * camera.near / params.focus; - const ymax = (camera.near * Math.tan(camera.state.fov * 0.5)) / /* cache.zoom */ 1; + const eyeSepHalf = props.eyeSeparation / 2; + const eyeSepOnProjection = eyeSepHalf * camera.near / props.focus; + const ymax = camera.near * Math.tan(camera.state.fov * 0.5); let xmin, xmax; // translate xOffset - eyeLeft[12] = - eyeSepHalf; + eyeLeft[12] = -eyeSepHalf; eyeRight[12] = eyeSepHalf; // for left eye - xmin = - ymax * params.aspect + eyeSepOnProjection; - xmax = ymax * params.aspect + eyeSepOnProjection; + xmin = -ymax * aspect + eyeSepOnProjection; + xmax = ymax * aspect + eyeSepOnProjection; left.projection[0] = 2 * camera.near / (xmax - xmin); left.projection[8] = (xmax + xmin) / (xmax - xmin); Mat4.mul(left.view, left.view, eyeLeft); + Mat4.mul(left.projectionView, left.projection, left.view); + Mat4.invert(left.inverseProjectionView, left.projectionView); // for right eye - xmin = - ymax * params.aspect - eyeSepOnProjection; - xmax = ymax * params.aspect - eyeSepOnProjection; + xmin = -ymax * aspect - eyeSepOnProjection; + xmax = ymax * aspect - eyeSepOnProjection; right.projection[0] = 2 * camera.near / (xmax - xmin); right.projection[8] = (xmax + xmin) / (xmax - xmin); Mat4.mul(right.view, right.view, eyeRight); + Mat4.mul(right.projectionView, right.projection, right.view); + Mat4.invert(right.inverseProjectionView, right.projectionView); } \ No newline at end of file diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index f484230e73473d1dafa2845680a820225ad35696..03bb32c4bd383d003012e680bce95883ea45e752 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -21,25 +21,31 @@ import { MarkerAction } from '../mol-util/marker-action'; import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci'; import { Camera } from './camera'; import { ParamDefinition as PD } from '../mol-util/param-definition'; -import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper'; +import { DebugHelperParams } from './helper/bounding-sphere-helper'; import { SetUtils } from '../mol-util/set'; import { Canvas3dInteractionHelper } from './helper/interaction-events'; import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing'; import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample'; -import { DrawPass } from './passes/draw'; -import { PickData, PickPass } from './passes/pick'; +import { PickData } from './passes/pick'; +import { PickHelper } from './passes/pick'; import { ImagePass, ImageProps } from './passes/image'; import { Sphere3D } from '../mol-math/geometry'; import { isDebugMode } from '../mol-util/debug'; import { CameraHelperParams } from './helper/camera-helper'; import { produce } from 'immer'; -import { HandleHelper, HandleHelperParams } from './helper/handle-helper'; +import { HandleHelperParams } from './helper/handle-helper'; import { StereoCamera, StereoCameraParams } from './camera/stereo'; +import { Helper } from './helper/helper'; +import { Passes } from './passes/passes'; export const Canvas3DParams = { camera: PD.Group({ - mode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']] as const, { label: 'Camera' }), + mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }), helper: PD.Group(CameraHelperParams, { isFlat: true }), + stereo: PD.MappedStatic('off', { + on: PD.Group(StereoCameraParams), + off: PD.Group({}) + }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }), manualReset: PD.Boolean(false, { isHidden: true }) }, { pivot: 'mode' }), cameraFog: PD.MappedStatic('on', { @@ -61,10 +67,6 @@ export const Canvas3DParams = { height: PD.Numeric(128) }) }), - stereo: PD.MappedStatic('off', { - on: PD.Group(StereoCameraParams), - off: PD.Group({}) - }, { cycle: true }), cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }), transparentBackground: PD.Boolean(false), @@ -136,7 +138,7 @@ namespace Canvas3D { export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 } export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, position?: Vec3 } - export function fromCanvas(canvas: HTMLCanvasElement, props: PartialCanvas3DProps = {}, attribs: Partial<{ antialias: boolean, pixelScale: number }> = {}) { + export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number }> = {}) { const gl = getGLContext(canvas, { alpha: true, antialias: attribs.antialias ?? true, @@ -149,6 +151,7 @@ namespace Canvas3D { const { pixelScale } = attribs; const input = InputObserver.fromElement(canvas, { pixelScale }); const webgl = createContext(gl, { pixelScale }); + const passes = new Passes(webgl, attribs); if (isDebugMode) { const loseContextExt = gl.getExtension('WEBGL_lose_context'); @@ -183,11 +186,11 @@ namespace Canvas3D { if (isDebugMode) console.log('context restored'); }, false); - return create(webgl, input, props, { pixelScale }); + return create(webgl, input, passes, props, { pixelScale }); } - export function create(webgl: WebGLContext, input: InputObserver, props: PartialCanvas3DProps = {}, attribs: Partial<{ pickScale: number, pixelScale: number }> = {}): Canvas3D { - const p = { ...DefaultCanvas3DParams, ...props }; + export function create(webgl: WebGLContext, input: InputObserver, passes: Passes, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ pixelScale: number }>): Canvas3D { + const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props }; const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>(); const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>(); @@ -212,23 +215,14 @@ namespace Canvas3D { fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0, clipFar: p.cameraClipping.far }, { x, y, width, height }, { pixelScale: attribs.pixelScale }); - const stereoCamera = new StereoCamera(); + const stereoCamera = new StereoCamera(camera, p.camera.stereo.params); const controls = TrackballControls.create(input, camera, p.trackball); const renderer = Renderer.create(webgl, p.renderer); - const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug); - const handleHelper = new HandleHelper(webgl, p.handle); - const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera); + const helper = new Helper(webgl, scene, p); - const drawPass = new DrawPass(webgl, renderer, scene, { standard: camera, stereo: stereoCamera }, debugHelper, handleHelper, { - cameraHelper: p.camera.helper - }); - drawPass.isStereo = p.stereo.name === 'on'; - const pickPass = new PickPass(webgl, renderer, scene, camera, stereoCamera, handleHelper, attribs.pickScale || 0.25, drawPass); - pickPass.isStereo = p.stereo.name === 'on'; - - const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing); - const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample); + const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }); + const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera); let drawPending = false; let cameraResetRequested = false; @@ -239,7 +233,7 @@ namespace Canvas3D { let loci: Loci = EmptyLoci; let repr: Representation.Any = Representation.Empty; if (pickingId) { - loci = handleHelper.getLoci(pickingId); + loci = helper.handle.getLoci(pickingId); reprRenderObjects.forEach((_, _repr) => { const _loci = _repr.getLoci(pickingId); if (!isEmptyLoci(_loci)) { @@ -260,15 +254,15 @@ namespace Canvas3D { if (repr) { changed = repr.mark(loci, action); } else { - changed = handleHelper.mark(loci, action); + changed = helper.handle.mark(loci, action); reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; }); } if (changed) { scene.update(void 0, true); - handleHelper.scene.update(void 0, true); - const prevPickDirty = pickPass.pickDirty; + helper.handle.scene.update(void 0, true); + const prevPickDirty = pickHelper.dirty; draw(true); - pickPass.pickDirty = prevPickDirty; // marking does not change picking buffers + pickHelper.dirty = prevPickDirty; // marking does not change picking buffers } } @@ -280,25 +274,24 @@ namespace Canvas3D { let didRender = false; controls.update(currentTime); - Viewport.set(camera.viewport, x, y, width, height); const cameraChanged = camera.update(); - const multiSampleChanged = multiSample.update(force || cameraChanged); - - const isStereo = p.stereo.name === 'on'; + const multiSampleChanged = passes.multiSample.update(force || cameraChanged, p.multiSample); if (force || cameraChanged || multiSampleChanged) { - if ((force || cameraChanged) && p.stereo.name === 'on') stereoCamera.update(camera, p.stereo.params); + let cam: Camera | StereoCamera = camera; + if (p.camera.stereo.name === 'on') { + stereoCamera.update(); + cam = stereoCamera; + } - renderer.setViewport(x, y, width, height); - // TODO: support stereo rendering in multisampling - if (!isStereo && multiSample.enabled) { - multiSample.render(true, p.transparentBackground); + if (MultiSamplePass.isEnabled(p.multiSample)) { + passes.multiSample.render(renderer, cam, scene, helper, true, p.transparentBackground, p); } else { - const toDrawingBuffer = !postprocessing.enabled && scene.volumes.renderables.length === 0; - drawPass.render(toDrawingBuffer, p.transparentBackground); - if (!toDrawingBuffer) postprocessing.render(true); + const toDrawingBuffer = !PostprocessingPass.isEnabled(p.postprocessing) && scene.volumes.renderables.length === 0; + passes.draw.render(renderer, cam, scene, helper, toDrawingBuffer, p.transparentBackground); + if (!toDrawingBuffer) passes.postprocessing.render(cam, true, p.postprocessing); } - pickPass.pickDirty = true; + pickHelper.dirty = true; didRender = true; } @@ -347,7 +340,8 @@ namespace Canvas3D { } function identify(x: number, y: number): PickData | undefined { - return webgl.isContextLost ? undefined : pickPass.identify(x, y); + const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera; + return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam); } function commit(isSynchronous: boolean = false) { @@ -356,7 +350,7 @@ namespace Canvas3D { if (allCommited) { resolveCameraReset(); if (forceDrawAfterAllCommited) { - if (debugHelper.isEnabled) debugHelper.update(); + if (helper.debug.isEnabled) helper.debug.update(); draw(true); forceDrawAfterAllCommited = false; } @@ -414,7 +408,7 @@ namespace Canvas3D { if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false; - if (debugHelper.isEnabled) debugHelper.update(); + if (helper.debug.isEnabled) helper.debug.update(); if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) { cameraResetRequested = true; } @@ -494,7 +488,8 @@ namespace Canvas3D { return { camera: { mode: camera.state.mode, - helper: { ...drawPass.props.cameraHelper }, + helper: { ...helper.camera.props }, + stereo: { ...p.camera.stereo }, manualReset: !!p.camera.manualReset }, cameraFog: camera.state.fog > 0 @@ -504,21 +499,18 @@ namespace Canvas3D { cameraResetDurationMs: p.cameraResetDurationMs, transparentBackground: p.transparentBackground, viewport: p.viewport, - stereo: p.stereo, - postprocessing: { ...postprocessing.props }, - multiSample: { ...multiSample.props }, + postprocessing: { ...p.postprocessing }, + multiSample: { ...p.multiSample }, renderer: { ...renderer.props }, trackball: { ...controls.props }, - debug: { ...debugHelper.props }, - handle: { ...handleHelper.props }, + debug: { ...helper.debug.props }, + handle: { ...helper.handle.props }, }; } - handleResize(); - const contextRestoredSub = contextRestored.subscribe(() => { - pickPass.pickDirty = true; + pickHelper.dirty = true; draw(true); }); @@ -542,7 +534,7 @@ namespace Canvas3D { reprUpdatedSubscriptions.clear(); reprRenderObjects.clear(); scene.clear(); - debugHelper.clear(); + helper.debug.clear(); requestDraw(true); reprCount.next(reprRenderObjects.size); }, @@ -553,7 +545,7 @@ namespace Canvas3D { } if (scene.syncVisibility()) { - if (debugHelper.isEnabled) debugHelper.update(); + if (helper.debug.isEnabled) helper.debug.update(); } requestDraw(true); }, @@ -565,7 +557,12 @@ namespace Canvas3D { mark, getLoci, - handleResize, + handleResize: () => { + passes.updateSize(); + updateViewport(); + syncViewport(); + requestDraw(true); + }, requestCameraReset: options => { nextCameraResetDuration = options?.durationMs; nextCameraResetSnapshot = options?.snapshot; @@ -602,31 +599,35 @@ namespace Canvas3D { } if (Object.keys(cameraState).length > 0) camera.setState(cameraState); - if (props.camera?.helper) drawPass.setProps({ cameraHelper: props.camera.helper }); + if (props.camera?.helper) helper.camera.setProps(props.camera.helper); if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset; + if (props.camera?.stereo !== undefined) Object.assign(p.camera.stereo, props.camera.stereo); if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs; if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground; if (props.viewport !== undefined) { + // clear old viewport + renderer.setViewport(x, y, width, height); + renderer.clear(p.transparentBackground); p.viewport = props.viewport; - handleResize(); - } - if (props.stereo !== undefined) { - p.stereo = props.stereo; - pickPass.isStereo = p.stereo.name === 'on'; - drawPass.isStereo = p.stereo.name === 'on'; + updateViewport(); + syncViewport(); } - if (props.postprocessing) postprocessing.setProps(props.postprocessing); - if (props.multiSample) multiSample.setProps(props.multiSample); + if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing); + if (props.multiSample) Object.assign(p.multiSample, props.multiSample); if (props.renderer) renderer.setProps(props.renderer); if (props.trackball) controls.setProps(props.trackball); - if (props.debug) debugHelper.setProps(props.debug); - if (props.handle) handleHelper.setProps(props.handle); + if (props.debug) helper.debug.setProps(props.debug); + if (props.handle) helper.handle.setProps(props.handle); + + if (cameraState.mode === 'orthographic') { + p.camera.stereo.name = 'off'; + } requestDraw(true); }, getImagePass: (props: Partial<ImageProps> = {}) => { - return new ImagePass(webgl, renderer, scene, camera, debugHelper, handleHelper, props); + return new ImagePass(webgl, renderer, scene, camera, helper, props); }, get props() { @@ -645,7 +646,7 @@ namespace Canvas3D { contextRestoredSub.unsubscribe(); scene.clear(); - debugHelper.clear(); + helper.debug.clear(); input.dispose(); controls.dispose(); renderer.dispose(); @@ -667,19 +668,11 @@ namespace Canvas3D { } } - function handleResize() { - updateViewport(); - + function syncViewport() { + pickHelper.setViewport(x, y, width, height); renderer.setViewport(x, y, width, height); Viewport.set(camera.viewport, x, y, width, height); Viewport.set(controls.viewport, x, y, width, height); - - drawPass.setSize(width, height); - pickPass.setSize(width, height); - postprocessing.setSize(width, height); - multiSample.setSize(width, height); - - requestDraw(true); } } } \ No newline at end of file diff --git a/src/mol-canvas3d/helper/helper.ts b/src/mol-canvas3d/helper/helper.ts new file mode 100644 index 0000000000000000000000000000000000000000..5db1c9234a6d9178b64ce8271f29cb88f2d866c8 --- /dev/null +++ b/src/mol-canvas3d/helper/helper.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import Scene from '../../mol-gl/scene'; +import { WebGLContext } from '../../mol-gl/webgl/context'; +import { ParamDefinition as PD } from '../../mol-util/param-definition'; +import { BoundingSphereHelper, DebugHelperParams } from './bounding-sphere-helper'; +import { CameraHelper, CameraHelperParams } from './camera-helper'; +import { HandleHelper, HandleHelperParams } from './handle-helper'; + +export const HelperParams = { + debug: PD.Group(DebugHelperParams), + camera: PD.Group({ + helper: PD.Group(CameraHelperParams) + }), + handle: PD.Group(HandleHelperParams), +}; +export const DefaultHelperProps = PD.getDefaultValues(HelperParams); +export type HelperProps = PD.Values<typeof HelperParams> + + +export class Helper { + readonly debug: BoundingSphereHelper + readonly camera: CameraHelper + readonly handle: HandleHelper + + constructor(webgl: WebGLContext, scene: Scene, props: Partial<HelperProps> = {}) { + const p = { ...DefaultHelperProps, ...props }; + + this.debug = new BoundingSphereHelper(webgl, scene, p.debug); + this.camera = new CameraHelper(webgl, p.camera.helper); + this.handle = new HandleHelper(webgl, p.handle); + } +} \ No newline at end of file diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts index aabc493527e698714f8dc2c67408725dc3203f12..85ce63a30cd0b4c880d3e4a4479697b819cdad5a 100644 --- a/src/mol-canvas3d/passes/draw.ts +++ b/src/mol-canvas3d/passes/draw.ts @@ -8,12 +8,8 @@ import { WebGLContext } from '../../mol-gl/webgl/context'; import { RenderTarget } from '../../mol-gl/webgl/render-target'; import Renderer from '../../mol-gl/renderer'; import Scene from '../../mol-gl/scene'; -import { BoundingSphereHelper } from '../helper/bounding-sphere-helper'; import { Texture } from '../../mol-gl/webgl/texture'; -import { ICamera } from '../camera'; -import { CameraHelper, CameraHelperParams } from '../helper/camera-helper'; -import { ParamDefinition as PD } from '../../mol-util/param-definition'; -import { HandleHelper } from '../helper/handle-helper'; +import { Camera, ICamera } from '../camera'; import { QuadSchema, QuadValues } from '../../mol-gl/compute/util'; import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema'; import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable'; @@ -21,10 +17,11 @@ import { ShaderCode } from '../../mol-gl/shader-code'; import { createComputeRenderItem } from '../../mol-gl/webgl/render-item'; import { ValueCell } from '../../mol-util'; import { Vec2 } from '../../mol-math/linear-algebra'; -import { StereoCamera } from '../camera/stereo'; +import { Helper } from '../helper/helper'; import quad_vert from '../../mol-gl/shader/quad.vert'; import depthMerge_frag from '../../mol-gl/shader/depth-merge.frag'; +import { StereoCamera } from '../camera/stereo'; const DepthMergeSchema = { ...QuadSchema, @@ -51,31 +48,20 @@ function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Text return createComputeRenderable(renderItem, values); } -export const DrawPassParams = { - cameraHelper: PD.Group(CameraHelperParams) -}; -export const DefaultDrawPassProps = PD.getDefaultValues(DrawPassParams); -export type DrawPassProps = PD.Values<typeof DrawPassParams> - export class DrawPass { readonly colorTarget: RenderTarget readonly depthTexture: Texture readonly depthTexturePrimitives: Texture - readonly packedDepth: boolean - - readonly cameraHelper: CameraHelper + private readonly packedDepth: boolean private depthTarget: RenderTarget private depthTargetPrimitives: RenderTarget | null private depthTargetVolumes: RenderTarget | null private depthTextureVolumes: Texture private depthMerge: DepthMergeRenderable - isStereo = false - - constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: { standard: ICamera, stereo?: StereoCamera }, private debugHelper: BoundingSphereHelper, private handleHelper: HandleHelper, props: Partial<DrawPassProps> = {}) { + constructor(private webgl: WebGLContext, width: number, height: number) { const { extensions, resources } = webgl; - const { width, height } = camera.standard.viewport; this.colorTarget = webgl.createRenderTarget(width, height); this.packedDepth = !extensions.depthTexture; @@ -93,70 +79,52 @@ export class DrawPass { this.depthTextureVolumes.define(width, height); } this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth); - - const p = { ...DefaultDrawPassProps, ...props }; - this.cameraHelper = new CameraHelper(webgl, p.cameraHelper); } setSize(width: number, height: number) { - this.colorTarget.setSize(width, height); - this.depthTarget.setSize(width, height); + const w = this.colorTarget.getWidth(); + const h = this.colorTarget.getHeight(); - if (this.depthTargetPrimitives) { - this.depthTargetPrimitives.setSize(width, height); - } else { - this.depthTexturePrimitives.define(width, height); - } - - if (this.depthTargetVolumes) { - this.depthTargetVolumes.setSize(width, height); - } else { - this.depthTextureVolumes.define(width, height); - } + if (width !== w || height !== h) { + this.colorTarget.setSize(width, height); + this.depthTarget.setSize(width, height); - ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height)); - } - - setProps(props: Partial<DrawPassProps>) { - if (props.cameraHelper) this.cameraHelper.setProps(props.cameraHelper); - } + if (this.depthTargetPrimitives) { + this.depthTargetPrimitives.setSize(width, height); + } else { + this.depthTexturePrimitives.define(width, height); + } - get props(): DrawPassProps { - return { - cameraHelper: { ...this.cameraHelper.props } - }; - } + if (this.depthTargetVolumes) { + this.depthTargetVolumes.setSize(width, height); + } else { + this.depthTextureVolumes.define(width, height); + } - render(toDrawingBuffer: boolean, transparentBackground: boolean) { - if (this.isStereo && this.camera.stereo) { - this._render(this.camera.stereo.left, toDrawingBuffer, transparentBackground); - this._render(this.camera.stereo.right, toDrawingBuffer, transparentBackground); - } else { - this._render(this.camera.standard, toDrawingBuffer, transparentBackground); + ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height)); } } - private _render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean) { + _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) { const { x, y, width, height } = camera.viewport; + renderer.setViewport(x, y, width, height); + if (toDrawingBuffer) { this.webgl.unbindFramebuffer(); - this.renderer.setViewport(x, y, width, height); } else { this.colorTarget.bind(); - this.renderer.setViewport(x, y, width, height); if (!this.packedDepth) { this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth'); } } - this.renderer.render(this.scene.primitives, camera, 'color', true, transparentBackground, null); + renderer.render(scene.primitives, camera, 'color', true, transparentBackground, 1, null); // do a depth pass if not rendering to drawing buffer and // extensions.depthTexture is unsupported (i.e. depthTarget is set) if (!toDrawingBuffer && this.depthTargetPrimitives) { this.depthTargetPrimitives.bind(); - this.renderer.setViewport(x, y, width, height); - this.renderer.render(this.scene.primitives, camera, 'depth', true, transparentBackground, null); + renderer.render(scene.primitives, camera, 'depth', true, transparentBackground, 1, null); this.colorTarget.bind(); } @@ -165,14 +133,16 @@ export class DrawPass { if (!this.packedDepth) { this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth'); this.webgl.state.depthMask(true); + this.webgl.gl.viewport(x, y, width, height); + this.webgl.gl.scissor(x, y, width, height); this.webgl.gl.clear(this.webgl.gl.DEPTH_BUFFER_BIT); } - this.renderer.render(this.scene.volumes, camera, 'color', false, transparentBackground, this.depthTexturePrimitives); + renderer.render(scene.volumes, camera, 'color', false, transparentBackground, 1, this.depthTexturePrimitives); // do volume depth pass if extensions.depthTexture is unsupported (i.e. depthTarget is set) if (this.depthTargetVolumes) { this.depthTargetVolumes.bind(); - this.renderer.render(this.scene.volumes, camera, 'depth', true, transparentBackground, this.depthTexturePrimitives); + renderer.render(scene.volumes, camera, 'depth', true, transparentBackground, 1, this.depthTexturePrimitives); this.colorTarget.bind(); } } @@ -181,28 +151,37 @@ export class DrawPass { if (!toDrawingBuffer) { this.depthMerge.update(); this.depthTarget.bind(); - this.renderer.setViewport(x, y, width, height); - this.webgl.state.disable(this.webgl.gl.SCISSOR_TEST); + // this.webgl.state.disable(this.webgl.gl.SCISSOR_TEST); this.webgl.state.disable(this.webgl.gl.BLEND); this.webgl.state.disable(this.webgl.gl.DEPTH_TEST); this.webgl.state.depthMask(false); this.webgl.state.clearColor(1, 1, 1, 1); + this.webgl.gl.viewport(x, y, width, height); + this.webgl.gl.scissor(x, y, width, height); this.webgl.gl.clear(this.webgl.gl.COLOR_BUFFER_BIT); this.depthMerge.render(); this.colorTarget.bind(); - this.renderer.setViewport(x, y, width, height); } - if (this.debugHelper.isEnabled) { - this.debugHelper.syncVisibility(); - this.renderer.render(this.debugHelper.scene, camera, 'color', false, transparentBackground, null); + if (helper.debug.isEnabled) { + helper.debug.syncVisibility(); + renderer.render(helper.debug.scene, camera, 'color', false, transparentBackground, 1, null); + } + if (helper.handle.isEnabled) { + renderer.render(helper.handle.scene, camera, 'color', false, transparentBackground, 1, null); } - if (this.handleHelper.isEnabled) { - this.renderer.render(this.handleHelper.scene, camera, 'color', false, transparentBackground, null); + if (helper.camera.isEnabled) { + helper.camera.update(camera); + renderer.render(helper.camera.scene, helper.camera.camera, 'color', false, transparentBackground, 1, null); } - if (this.cameraHelper.isEnabled) { - this.cameraHelper.update(camera); - this.renderer.render(this.cameraHelper.scene, this.cameraHelper.camera, 'color', false, transparentBackground, null); + } + + render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) { + if (StereoCamera.is(camera)) { + this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground); + this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground); + } else { + this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground); } } } \ No newline at end of file diff --git a/src/mol-canvas3d/passes/image.ts b/src/mol-canvas3d/passes/image.ts index f3a833987ed15736c42d0bcd2759fd48d99eff6d..c07c821c311fa791a5ff36aed36290c141185ce0 100644 --- a/src/mol-canvas3d/passes/image.ts +++ b/src/mol-canvas3d/passes/image.ts @@ -8,21 +8,22 @@ import { WebGLContext } from '../../mol-gl/webgl/context'; import { RenderTarget } from '../../mol-gl/webgl/render-target'; import Renderer from '../../mol-gl/renderer'; import Scene from '../../mol-gl/scene'; -import { BoundingSphereHelper } from '../helper/bounding-sphere-helper'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; -import { DrawPass, DrawPassParams } from './draw'; +import { DrawPass } from './draw'; import { PostprocessingPass, PostprocessingParams } from './postprocessing'; import { MultiSamplePass, MultiSampleParams } from './multi-sample'; import { Camera } from '../camera'; import { Viewport } from '../camera/util'; -import { HandleHelper } from '../helper/handle-helper'; import { PixelData } from '../../mol-util/image'; +import { Helper } from '../helper/helper'; +import { CameraHelper, CameraHelperParams } from '../helper/camera-helper'; export const ImageParams = { transparentBackground: PD.Boolean(false), multiSample: PD.Group(MultiSampleParams), postprocessing: PD.Group(PostprocessingParams), - drawPass: PD.Group(DrawPassParams), + + cameraHelper: PD.Group(CameraHelperParams), }; export type ImageProps = PD.Values<typeof ImageParams> @@ -30,26 +31,33 @@ export class ImagePass { private _width = 1024 private _height = 768 private _camera = new Camera() - private _transparentBackground = false + + readonly props: ImageProps private _colorTarget: RenderTarget get colorTarget() { return this._colorTarget; } readonly drawPass: DrawPass + private readonly postprocessing: PostprocessingPass private readonly multiSample: MultiSamplePass + private readonly helper: Helper get width() { return this._width; } get height() { return this._height; } - constructor(private webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, handleHelper: HandleHelper, props: Partial<ImageProps>) { - const p = { ...PD.getDefaultValues(ImageParams), ...props }; + constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, props: Partial<ImageProps>) { + this.props = { ...PD.getDefaultValues(ImageParams), ...props }; - this._transparentBackground = p.transparentBackground; + this.drawPass = new DrawPass(webgl, 128, 128); + this.postprocessing = new PostprocessingPass(webgl, this.drawPass); + this.multiSample = new MultiSamplePass(webgl, this.drawPass, this.postprocessing); - this.drawPass = new DrawPass(webgl, renderer, scene, { standard: this._camera }, debugHelper, handleHelper, p.drawPass); - this.postprocessing = new PostprocessingPass(webgl, this._camera, this.drawPass, p.postprocessing); - this.multiSample = new MultiSamplePass(webgl, this._camera, this.drawPass, this.postprocessing, p.multiSample); + this.helper = { + camera: new CameraHelper(webgl, this.props.cameraHelper), + debug: helper.debug, + handle: helper.handle, + }; this.setSize(this._width, this._height); } @@ -61,24 +69,13 @@ export class ImagePass { this._height = height; this.drawPass.setSize(width, height); - this.postprocessing.setSize(width, height); - this.multiSample.setSize(width, height); + this.postprocessing.syncSize(); + this.multiSample.syncSize(); } setProps(props: Partial<ImageProps> = {}) { - if (props.transparentBackground !== undefined) this._transparentBackground = props.transparentBackground; - if (props.postprocessing) this.postprocessing.setProps(props.postprocessing); - if (props.multiSample) this.multiSample.setProps(props.multiSample); - if (props.drawPass) this.drawPass.setProps(props.drawPass); - } - - get props(): ImageProps { - return { - transparentBackground: this._transparentBackground, - postprocessing: this.postprocessing.props, - multiSample: this.multiSample.props, - drawPass: this.drawPass.props - }; + Object.assign(this.props, props); + if (props.cameraHelper) this.helper.camera.setProps(props.cameraHelper); } render() { @@ -88,13 +85,13 @@ export class ImagePass { this.renderer.setViewport(0, 0, this._width, this._height); - if (this.multiSample.enabled) { - this.multiSample.render(false, this._transparentBackground); + if (MultiSamplePass.isEnabled(this.props.multiSample)) { + this.multiSample.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props); this._colorTarget = this.multiSample.colorTarget; } else { - this.drawPass.render(false, this._transparentBackground); - if (this.postprocessing.enabled) { - this.postprocessing.render(false); + this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground); + if (PostprocessingPass.isEnabled(this.props.postprocessing)) { + this.postprocessing.render(this._camera, false, this.props.postprocessing); this._colorTarget = this.postprocessing.target; } else { this._colorTarget = this.drawPass.colorTarget; diff --git a/src/mol-canvas3d/passes/multi-sample.ts b/src/mol-canvas3d/passes/multi-sample.ts index d75964b4174112145211a8d011bb91cf6f4cf0aa..5f7c8e03e432fccd9c43e0c46213ce6db84e1e6a 100644 --- a/src/mol-canvas3d/passes/multi-sample.ts +++ b/src/mol-canvas3d/passes/multi-sample.ts @@ -16,8 +16,12 @@ import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/rendera import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { RenderTarget } from '../../mol-gl/webgl/render-target'; import { Camera } from '../../mol-canvas3d/camera'; -import { PostprocessingPass } from './postprocessing'; +import { PostprocessingPass, PostprocessingProps } from './postprocessing'; import { DrawPass } from './draw'; +import Renderer from '../../mol-gl/renderer'; +import Scene from '../../mol-gl/scene'; +import { Helper } from '../helper/helper'; +import { StereoCamera } from '../camera/stereo'; import quad_vert from '../../mol-gl/shader/quad.vert'; import compose_frag from '../../mol-gl/shader/compose.frag'; @@ -51,8 +55,13 @@ export const MultiSampleParams = { }; export type MultiSampleProps = PD.Values<typeof MultiSampleParams> +type Props = { multiSample: MultiSampleProps, postprocessing: PostprocessingProps } + export class MultiSamplePass { - props: MultiSampleProps + static isEnabled(props: MultiSampleProps) { + return props.mode !== 'off'; + } + colorTarget: RenderTarget private composeTarget: RenderTarget @@ -61,25 +70,25 @@ export class MultiSamplePass { private sampleIndex = -2 - constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) { - const { gl, extensions } = webgl; - this.colorTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false); - this.composeTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false, extensions.colorBufferFloat ? 'float32' : 'uint8'); - this.holdTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight, false); + constructor(private webgl: WebGLContext, private drawPass: DrawPass, private postprocessing: PostprocessingPass) { + const { extensions } = webgl; + const width = drawPass.colorTarget.getWidth(); + const height = drawPass.colorTarget.getHeight(); + this.colorTarget = webgl.createRenderTarget(width, height, false); + this.composeTarget = webgl.createRenderTarget(width, height, false, extensions.colorBufferFloat ? 'float32' : 'uint8'); + this.holdTarget = webgl.createRenderTarget(width, height, false); this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture); - this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props }; } - get enabled() { - return this.props.mode !== 'off'; - } - - update(changed: boolean) { + update(changed: boolean, props: MultiSampleProps) { if (changed) this.sampleIndex = -1; - return this.props.mode === 'temporal' ? this.sampleIndex !== -2 : false; + return props.mode === 'temporal' ? this.sampleIndex !== -2 : false; } - setSize(width: number, height: number) { + syncSize() { + const width = this.drawPass.colorTarget.getWidth(); + const height = this.drawPass.colorTarget.getHeight(); + const [w, h] = this.compose.values.uTexSize.ref.value; if (width !== w || height !== h) { this.colorTarget.setSize(width, height); @@ -89,27 +98,16 @@ export class MultiSamplePass { } } - setProps(props: Partial<MultiSampleProps>) { - if (props.mode !== undefined) this.props.mode = props.mode; - if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel; - } - - render(toDrawingBuffer: boolean, transparentBackground: boolean) { - if (this.props.mode === 'temporal') { - this.renderTemporalMultiSample(toDrawingBuffer, transparentBackground); + render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) { + if (props.multiSample.mode === 'temporal') { + this.renderTemporalMultiSample(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props); } else { - this.renderMultiSample(toDrawingBuffer, transparentBackground); + this.renderMultiSample(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props); } } - private setQuadShift(x: number, y: number) { - ValueCell.update(this.compose.values.uQuadShift, Vec2.set( - this.compose.values.uQuadShift.ref.value, x, y) - ); - } - - private renderMultiSample(toDrawingBuffer: boolean, transparentBackground: boolean) { - const { camera, compose, composeTarget, drawPass, postprocessing, webgl } = this; + private renderMultiSample(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) { + const { compose, composeTarget, drawPass, postprocessing, webgl } = this; const { gl, state } = webgl; // based on the Multisample Anti-Aliasing Render Pass @@ -117,24 +115,24 @@ export class MultiSamplePass { // // This manual approach to MSAA re-renders the scene once for // each sample with camera jitter and accumulates the results. - const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ]; + const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ]; + const { x, y, width, height } = camera.viewport; const baseSampleWeight = 1.0 / offsetList.length; const roundingRange = 1 / 32; + const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing); + camera.viewOffset.enabled = true; - ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture); + ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture); compose.update(); - const { x, y, width, height } = camera.viewport; - // render the scene multiple times, each slightly jitter offset // from the last and accumulate the results. for (let i = 0; i < offsetList.length; ++i) { const offset = offsetList[i]; Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height); camera.update(); - this.drawPass.cameraHelper.update(camera); // the theory is that equal weights for each sample lead to an accumulation of rounding // errors. The following equation varies the sampleWeight per sample so that it is uniformly @@ -144,8 +142,8 @@ export class MultiSamplePass { ValueCell.update(compose.values.uWeight, sampleWeight); // render scene and optionally postprocess - drawPass.render(false, transparentBackground); - if (postprocessing.enabled) postprocessing.render(false); + drawPass.render(renderer, camera, scene, helper, false, transparentBackground); + if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing); // compose rendered scene with compose target composeTarget.bind(); @@ -153,15 +151,13 @@ export class MultiSamplePass { state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); state.disable(gl.DEPTH_TEST); - state.disable(gl.SCISSOR_TEST); state.depthMask(false); + gl.viewport(x, y, width, height); + gl.scissor(x, y, width, height); if (i === 0) { state.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); } - this.setQuadShift(0, 0); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); compose.render(); } @@ -174,9 +170,10 @@ export class MultiSamplePass { } else { this.colorTarget.bind(); } - this.setQuadShift(x / width, y / height); + gl.viewport(x, y, width, height); gl.scissor(x, y, width, height); + state.disable(gl.BLEND); compose.render(); @@ -184,8 +181,8 @@ export class MultiSamplePass { camera.update(); } - private renderTemporalMultiSample(toDrawingBuffer: boolean, transparentBackground: boolean) { - const { camera, compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this; + private renderTemporalMultiSample(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) { + const { compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this; const { gl, state } = webgl; // based on the Multisample Anti-Aliasing Render Pass @@ -193,7 +190,7 @@ export class MultiSamplePass { // // This manual approach to MSAA re-renders the scene once for // each sample with camera jitter and accumulates the results. - const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ]; + const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ]; if (this.sampleIndex === -2) return; if (this.sampleIndex >= offsetList.length) { @@ -203,42 +200,40 @@ export class MultiSamplePass { const { x, y, width, height } = camera.viewport; const sampleWeight = 1.0 / offsetList.length; + const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing); if (this.sampleIndex === -1) { - drawPass.render(false, transparentBackground); - if (postprocessing.enabled) postprocessing.render(false); + drawPass.render(renderer, camera, scene, helper, false, transparentBackground); + if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing); ValueCell.update(compose.values.uWeight, 1.0); - ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture); + ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture); compose.update(); holdTarget.bind(); state.disable(gl.BLEND); state.disable(gl.DEPTH_TEST); - state.disable(gl.SCISSOR_TEST); state.depthMask(false); - this.setQuadShift(0, 0); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); + gl.viewport(x, y, width, height); + gl.scissor(x, y, width, height); compose.render(); this.sampleIndex += 1; } else { camera.viewOffset.enabled = true; - ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture); + ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture); ValueCell.update(compose.values.uWeight, sampleWeight); compose.update(); // render the scene multiple times, each slightly jitter offset // from the last and accumulate the results. - const numSamplesPerFrame = Math.pow(2, Math.max(0, this.props.sampleLevel - 2)); + const numSamplesPerFrame = Math.pow(2, Math.max(0, props.multiSample.sampleLevel - 2)); for (let i = 0; i < numSamplesPerFrame; ++i) { const offset = offsetList[this.sampleIndex]; Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height); camera.update(); - this.drawPass.cameraHelper.update(camera); // render scene and optionally postprocess - drawPass.render(false, transparentBackground); - if (postprocessing.enabled) postprocessing.render(false); + drawPass.render(renderer, camera, scene, helper, false, transparentBackground); + if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing); // compose rendered scene with compose target composeTarget.bind(); @@ -246,15 +241,13 @@ export class MultiSamplePass { state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD); state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE); state.disable(gl.DEPTH_TEST); - state.disable(gl.SCISSOR_TEST); state.depthMask(false); + gl.viewport(x, y, width, height); + gl.scissor(x, y, width, height); if (this.sampleIndex === 0) { state.clearColor(0, 0, 0, 0); gl.clear(gl.COLOR_BUFFER_BIT); } - this.setQuadShift(0, 0); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); compose.render(); this.sampleIndex += 1; @@ -264,16 +257,13 @@ export class MultiSamplePass { if (toDrawingBuffer) { webgl.unbindFramebuffer(); - this.setQuadShift(x / width, y / height); - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); } else { this.colorTarget.bind(); - this.setQuadShift(0, 0); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); } + gl.viewport(x, y, width, height); + gl.scissor(x, y, width, height); + const accumulationWeight = this.sampleIndex * sampleWeight; if (accumulationWeight > 0) { ValueCell.update(compose.values.uWeight, 1.0); diff --git a/src/mol-canvas3d/passes/passes.ts b/src/mol-canvas3d/passes/passes.ts new file mode 100644 index 0000000000000000000000000000000000000000..24660ab3f49d6f7af26f914a040ac30321b857dd --- /dev/null +++ b/src/mol-canvas3d/passes/passes.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { DrawPass } from './draw'; +import { PickPass } from './pick'; +import { PostprocessingPass } from './postprocessing'; +import { MultiSamplePass } from './multi-sample'; +import { WebGLContext } from '../../mol-gl/webgl/context'; + +export class Passes { + readonly draw: DrawPass + readonly pick: PickPass + readonly postprocessing: PostprocessingPass + readonly multiSample: MultiSamplePass + + constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number }> = {}) { + const { gl } = webgl; + this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight); + this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25); + this.postprocessing = new PostprocessingPass(webgl, this.draw); + this.multiSample = new MultiSamplePass(webgl, this.draw, this.postprocessing); + } + + updateSize() { + const { gl } = this.webgl; + this.draw.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight); + this.pick.syncSize(); + this.postprocessing.syncSize(); + this.multiSample.syncSize(); + } +} \ No newline at end of file diff --git a/src/mol-canvas3d/passes/pick.ts b/src/mol-canvas3d/passes/pick.ts index 7de7688e0ab906dd9de21d2a63fa9efa2192d698..348180eae64322f32fd96755a4e9bee70fa347d4 100644 --- a/src/mol-canvas3d/passes/pick.ts +++ b/src/mol-canvas3d/passes/pick.ts @@ -15,7 +15,8 @@ import { decodeFloatRGB, unpackRGBAToDepth } from '../../mol-util/float-packing' import { Camera, ICamera } from '../camera'; import { StereoCamera } from '../camera/stereo'; import { cameraUnproject } from '../camera/util'; -import { HandleHelper } from '../helper/handle-helper'; +import { Viewport } from '../camera/util'; +import { Helper } from '../helper/helper'; import { DrawPass } from './draw'; const NullId = Math.pow(2, 24) - 2; @@ -23,51 +24,33 @@ const NullId = Math.pow(2, 24) - 2; export type PickData = { id: PickingId, position: Vec3 } export class PickPass { - pickDirty = true + readonly objectPickTarget: RenderTarget + readonly instancePickTarget: RenderTarget + readonly groupPickTarget: RenderTarget + readonly depthPickTarget: RenderTarget - objectPickTarget: RenderTarget - instancePickTarget: RenderTarget - groupPickTarget: RenderTarget - depthPickTarget: RenderTarget - - isStereo = false - - private objectBuffer: Uint8Array - private instanceBuffer: Uint8Array - private groupBuffer: Uint8Array - private depthBuffer: Uint8Array - - private pickScale: number private pickWidth: number private pickHeight: number - constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private stereoCamera: StereoCamera, private handleHelper: HandleHelper, private pickBaseScale: number, private drawPass: DrawPass) { - this.pickScale = pickBaseScale / webgl.pixelRatio; - this.pickWidth = Math.ceil(camera.viewport.width * this.pickScale); - this.pickHeight = Math.ceil(camera.viewport.height * this.pickScale); + constructor(private webgl: WebGLContext, private drawPass: DrawPass, readonly pickBaseScale: number) { + const pickScale = pickBaseScale / webgl.pixelRatio; + this.pickWidth = Math.ceil(drawPass.colorTarget.getWidth() * pickScale); + this.pickHeight = Math.ceil(drawPass.colorTarget.getHeight() * pickScale); this.objectPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); this.instancePickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); this.groupPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); this.depthPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight); - - this.setupBuffers(); } - private setupBuffers() { - const bufferSize = this.pickWidth * this.pickHeight * 4; - if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) { - this.objectBuffer = new Uint8Array(bufferSize); - this.instanceBuffer = new Uint8Array(bufferSize); - this.groupBuffer = new Uint8Array(bufferSize); - this.depthBuffer = new Uint8Array(bufferSize); - } + get drawingBufferHeight() { + return this.drawPass.colorTarget.getHeight(); } - setSize(width: number, height: number) { - this.pickScale = this.pickBaseScale / this.webgl.pixelRatio; - const pickWidth = Math.ceil(width * this.pickScale); - const pickHeight = Math.ceil(height * this.pickScale); + syncSize() { + const pickScale = this.pickBaseScale / this.webgl.pixelRatio; + const pickWidth = Math.ceil(this.drawPass.colorTarget.getWidth() * pickScale); + const pickHeight = Math.ceil(this.drawPass.colorTarget.getHeight() * pickScale); if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) { this.pickWidth = pickWidth; @@ -77,65 +60,92 @@ export class PickPass { this.instancePickTarget.setSize(this.pickWidth, this.pickHeight); this.groupPickTarget.setSize(this.pickWidth, this.pickHeight); this.depthPickTarget.setSize(this.pickWidth, this.pickHeight); - - this.setupBuffers(); - } - } - - private renderVariant(variant: GraphicsRenderVariant) { - if (this.isStereo) { - const w = (this.pickWidth / 2) | 0; - - this.renderer.setViewport(0, 0, w, this.pickHeight); - this._renderVariant(this.stereoCamera.left, variant); - - this.renderer.setViewport(w, 0, this.pickWidth - w, this.pickHeight); - this._renderVariant(this.stereoCamera.right, variant); - } else { - this.renderer.setViewport(0, 0, this.pickWidth, this.pickHeight); - this._renderVariant(this.camera, variant); } } - private _renderVariant(camera: ICamera, variant: GraphicsRenderVariant) { - const { renderer, scene, handleHelper: { scene: handleScene } } = this; + private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: GraphicsRenderVariant) { + const pickScale = this.pickBaseScale / this.webgl.pixelRatio; const depth = this.drawPass.depthTexturePrimitives; - - renderer.render(scene.primitives, camera, variant, true, false, null); - renderer.render(scene.volumes, camera, variant, false, false, depth); - renderer.render(handleScene, camera, variant, false, false, null); + renderer.render(scene.primitives, camera, variant, true, false, pickScale, null); + renderer.render(scene.volumes, camera, variant, false, false, pickScale, depth); + renderer.render(helper.handle.scene, camera, variant, false, false, pickScale, null); } - render() { + render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper) { this.objectPickTarget.bind(); - this.renderVariant('pickObject'); + this.renderVariant(renderer, camera, scene, helper, 'pickObject'); this.instancePickTarget.bind(); - this.renderVariant('pickInstance'); + this.renderVariant(renderer, camera, scene, helper, 'pickInstance'); this.groupPickTarget.bind(); - this.renderVariant('pickGroup'); + this.renderVariant(renderer, camera, scene, helper, 'pickGroup'); this.depthPickTarget.bind(); - this.renderVariant('depth'); + this.renderVariant(renderer, camera, scene, helper, 'depth'); + } +} + +export class PickHelper { + dirty = true + + private objectBuffer: Uint8Array + private instanceBuffer: Uint8Array + private groupBuffer: Uint8Array + private depthBuffer: Uint8Array + + private viewport = Viewport() + + private pickScale: number + private pickX: number + private pickY: number + private pickWidth: number + private pickHeight: number + private halfPickWidth: number - this.pickDirty = false; + private setupBuffers() { + const bufferSize = this.pickWidth * this.pickHeight * 4; + if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) { + this.objectBuffer = new Uint8Array(bufferSize); + this.instanceBuffer = new Uint8Array(bufferSize); + this.groupBuffer = new Uint8Array(bufferSize); + this.depthBuffer = new Uint8Array(bufferSize); + } + } + + setViewport(x: number, y: number, width: number, height: number) { + Viewport.set(this.viewport, x, y, width, height); + + this.pickScale = this.pickPass.pickBaseScale / this.webgl.pixelRatio; + this.pickX = Math.ceil(x * this.pickScale); + this.pickY = Math.ceil(y * this.pickScale); + + const pickWidth = Math.ceil(width * this.pickScale); + const pickHeight = Math.ceil(height * this.pickScale); + + if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) { + this.pickWidth = pickWidth; + this.pickHeight = pickHeight; + this.halfPickWidth = Math.floor(this.pickWidth / 2); + + this.setupBuffers(); + } } private syncBuffers() { - const { webgl } = this; + const { pickX, pickY, pickWidth, pickHeight } = this; - this.objectPickTarget.bind(); - webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.objectBuffer); + this.pickPass.objectPickTarget.bind(); + this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.objectBuffer); - this.instancePickTarget.bind(); - webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.instanceBuffer); + this.pickPass.instancePickTarget.bind(); + this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.instanceBuffer); - this.groupPickTarget.bind(); - webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.groupBuffer); + this.pickPass.groupPickTarget.bind(); + this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.groupBuffer); - this.depthPickTarget.bind(); - webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.depthBuffer); + this.pickPass.depthPickTarget.bind(); + this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.depthBuffer); } private getBufferIdx(x: number, y: number): number { @@ -153,34 +163,52 @@ export class PickPass { return decodeFloatRGB(buffer[idx], buffer[idx + 1], buffer[idx + 2]); } - identify(x: number, y: number): PickData | undefined { - const { webgl, pickScale, camera: { viewport } } = this; + private render(camera: Camera | StereoCamera) { + const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this; + + const { renderer, scene, helper } = this; + + if (StereoCamera.is(camera)) { + this.renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight); + this.pickPass.render(renderer, camera.left, scene, helper); + + this.renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight); + this.pickPass.render(renderer, camera.right, scene, helper); + } else { + this.renderer.setViewport(pickX, pickY, pickWidth, pickHeight); + this.pickPass.render(renderer, camera, scene, helper); + } + + this.dirty = false; + } + + identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined { + const { webgl, pickScale } = this; if (webgl.isContextLost) return; - const { gl, pixelRatio } = webgl; - x *= pixelRatio; - y *= pixelRatio; + x *= webgl.pixelRatio; + y *= webgl.pixelRatio; + y = this.pickPass.drawingBufferHeight - y; // flip y + + const { viewport } = this; // check if within viewport if (x < viewport.x || - gl.drawingBufferHeight - y < viewport.y || + y < viewport.y || x > viewport.x + viewport.width || - gl.drawingBufferHeight - y > viewport.y + viewport.height - ) { - return; - } + y > viewport.y + viewport.height + ) return; - if (this.pickDirty) { - this.render(); + if (this.dirty) { + this.render(camera); this.syncBuffers(); } - x -= viewport.x; - y += viewport.y; // plus because of flipped y - y = gl.drawingBufferHeight - y; // flip y + const xv = x - viewport.x; + const yv = y - viewport.y; - const xp = Math.floor(x * pickScale); - const yp = Math.floor(y * pickScale); + const xp = Math.floor(xv * pickScale); + const yp = Math.floor(yv * pickScale); const objectId = this.getId(xp, yp, this.objectBuffer); // console.log('objectId', objectId); @@ -195,10 +223,25 @@ export class PickPass { if (groupId === -1 || groupId === NullId) return; const z = this.getDepth(xp, yp); - const position = Vec3.create(x, gl.drawingBufferHeight - y, z); - cameraUnproject(position, position, viewport, this.camera.inverseProjectionView); + const position = Vec3.create(x, viewport.height - y, z); + if (StereoCamera.is(camera)) { + const halfWidth = Math.floor(viewport.width / 2); + if (x > viewport.x + halfWidth) { + position[0] = viewport.x + (xv - halfWidth) * 2; + cameraUnproject(position, position, viewport, camera.right.inverseProjectionView); + } else { + position[0] = viewport.x + xv * 2; + cameraUnproject(position, position, viewport, camera.left.inverseProjectionView); + } + } else { + cameraUnproject(position, position, viewport, camera.inverseProjectionView); + } // console.log({ { objectId, instanceId, groupId }, position} ); return { id: { objectId, instanceId, groupId }, position }; } + + constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport) { + this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height); + } } \ No newline at end of file diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts index 1825cb24dcb842da70fc4b19b85ca3fc58c741d6..49aa45fb12c9b9b84fff558a65aec9839d18b53c 100644 --- a/src/mol-canvas3d/passes/postprocessing.ts +++ b/src/mol-canvas3d/passes/postprocessing.ts @@ -16,11 +16,11 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra'; import { ParamDefinition as PD } from '../../mol-util/param-definition'; import { RenderTarget } from '../../mol-gl/webgl/render-target'; import { DrawPass } from './draw'; -import { Camera } from '../../mol-canvas3d/camera'; -import { produce } from 'immer'; +import { Camera, ICamera } from '../../mol-canvas3d/camera'; import quad_vert from '../../mol-gl/shader/quad.vert'; import postprocessing_frag from '../../mol-gl/shader/postprocessing.frag'; +import { StereoCamera } from '../camera/stereo'; const PostprocessingSchema = { ...QuadSchema, @@ -47,8 +47,7 @@ const PostprocessingSchema = { const PostprocessingShaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag); type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>> -function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, props: Partial<PostprocessingProps>): PostprocessingRenderable { - const p = { ...PD.getDefaultValues(PostprocessingParams), ...props }; +function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture): PostprocessingRenderable { const values: Values<typeof PostprocessingSchema> = { ...QuadValues, tColor: ValueCell.create(colorTexture), @@ -62,14 +61,14 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d uFogFar: ValueCell.create(10000), uFogColor: ValueCell.create(Vec3.create(1, 1, 1)), - dOcclusionEnable: ValueCell.create(p.occlusion.name === 'on'), - dOcclusionKernelSize: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.kernelSize : 4), - uOcclusionBias: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.bias : 0.5), - uOcclusionRadius: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.radius : 64), + dOcclusionEnable: ValueCell.create(false), + dOcclusionKernelSize: ValueCell.create(4), + uOcclusionBias: ValueCell.create(0.5), + uOcclusionRadius: ValueCell.create(64), - dOutlineEnable: ValueCell.create(p.outline.name === 'on'), - uOutlineScale: ValueCell.create((p.outline.name === 'on' ? p.outline.params.scale : 1) * ctx.pixelRatio), - uOutlineThreshold: ValueCell.create(p.outline.name === 'on' ? p.outline.params.threshold : 0.8), + dOutlineEnable: ValueCell.create(false), + uOutlineScale: ValueCell.create(1 * ctx.pixelRatio), + uOutlineThreshold: ValueCell.create(0.8), }; const schema = { ...PostprocessingSchema }; @@ -98,22 +97,23 @@ export const PostprocessingParams = { export type PostprocessingProps = PD.Values<typeof PostprocessingParams> export class PostprocessingPass { + static isEnabled(props: PostprocessingProps) { + return props.occlusion.name === 'on' || props.outline.name === 'on'; + } + target: RenderTarget - props: PostprocessingProps renderable: PostprocessingRenderable - constructor(private webgl: WebGLContext, private camera: Camera, drawPass: DrawPass, props: Partial<PostprocessingProps>) { - this.target = webgl.createRenderTarget(camera.viewport.width, camera.viewport.height, false); - this.props = { ...PD.getDefaultValues(PostprocessingParams), ...props }; - const { colorTarget, depthTexture, packedDepth } = drawPass; - this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture, packedDepth, this.props); + constructor(private webgl: WebGLContext, private drawPass: DrawPass) { + this.target = webgl.createRenderTarget(drawPass.colorTarget.getWidth(), drawPass.colorTarget.getHeight(), false); + const { colorTarget, depthTexture } = drawPass; + this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture); } - get enabled() { - return this.props.occlusion.name === 'on' || this.props.outline.name === 'on'; - } + syncSize() { + const width = this.drawPass.colorTarget.getWidth(); + const height = this.drawPass.colorTarget.getHeight(); - setSize(width: number, height: number) { const [w, h] = this.renderable.values.uTexSize.ref.value; if (width !== w || height !== h) { this.target.setSize(width, height); @@ -121,69 +121,67 @@ export class PostprocessingPass { } } - setProps(props: Partial<PostprocessingProps>) { - this.props = produce(this.props, p => { - if (props.occlusion !== undefined) { - p.occlusion.name = props.occlusion.name; - ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, props.occlusion.name === 'on'); - if (props.occlusion.name === 'on') { - p.occlusion.params = { ...props.occlusion.params }; - ValueCell.updateIfChanged(this.renderable.values.dOcclusionKernelSize, props.occlusion.params.kernelSize); - ValueCell.updateIfChanged(this.renderable.values.uOcclusionBias, props.occlusion.params.bias); - ValueCell.updateIfChanged(this.renderable.values.uOcclusionRadius, props.occlusion.params.radius); - } - } - - if (props.outline !== undefined) { - p.outline.name = props.outline.name; - ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, props.outline.name === 'on'); - if (props.outline.name === 'on') { - p.outline.params = { ...props.outline.params }; - ValueCell.updateIfChanged(this.renderable.values.uOutlineScale, props.outline.params.scale); - ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, props.outline.params.threshold); - } - } - }); - - this.renderable.update(); - } - - private setQuadShift(x: number, y: number) { - ValueCell.update(this.renderable.values.uQuadShift, Vec2.set( - this.renderable.values.uQuadShift.ref.value, x, y) - ); - } + _render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) { + const { values } = this.renderable; + + ValueCell.updateIfChanged(values.uFar, camera.far); + ValueCell.updateIfChanged(values.uNear, camera.near); + ValueCell.updateIfChanged(values.uFogFar, camera.fogFar); + ValueCell.updateIfChanged(values.uFogNear, camera.fogNear); + + let needsUpdate = false; + + const orthographic = camera.state.mode === 'orthographic' ? 1 : 0; + if (values.dOrthographic.ref.value !== orthographic) needsUpdate = true; + ValueCell.updateIfChanged(values.dOrthographic, orthographic); + + const occlusion = props.occlusion.name === 'on'; + if (values.dOcclusionEnable.ref.value !== occlusion) needsUpdate = true; + ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusion); + if (props.occlusion.name === 'on') { + const { kernelSize } = props.occlusion.params; + if (values.dOcclusionKernelSize.ref.value !== kernelSize) needsUpdate = true; + ValueCell.updateIfChanged(values.dOcclusionKernelSize, kernelSize); + ValueCell.updateIfChanged(values.uOcclusionBias, props.occlusion.params.bias); + ValueCell.updateIfChanged(values.uOcclusionRadius, props.occlusion.params.radius); + } - render(toDrawingBuffer: boolean) { - ValueCell.updateIfChanged(this.renderable.values.uFar, this.camera.far); - ValueCell.updateIfChanged(this.renderable.values.uNear, this.camera.near); - ValueCell.updateIfChanged(this.renderable.values.uFogFar, this.camera.fogFar); - ValueCell.updateIfChanged(this.renderable.values.uFogNear, this.camera.fogNear); + const outline = props.outline.name === 'on'; + if (values.dOutlineEnable.ref.value !== outline) needsUpdate = true; + ValueCell.updateIfChanged(values.dOutlineEnable, outline); + if (props.outline.name === 'on') { + ValueCell.updateIfChanged(values.uOutlineScale, props.outline.params.scale * this.webgl.pixelRatio); + ValueCell.updateIfChanged(values.uOutlineThreshold, props.outline.params.threshold); + } - const orthographic = this.camera.state.mode === 'orthographic' ? 1 : 0; - if (this.renderable.values.dOrthographic.ref.value !== orthographic) { - ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic); + if (needsUpdate) { this.renderable.update(); } - const { x, y, width, height } = this.camera.viewport; - const { gl, state } = this.webgl; if (toDrawingBuffer) { this.webgl.unbindFramebuffer(); - this.setQuadShift(x / width, y / height); - gl.viewport(x, y, width, height); - gl.scissor(x, y, width, height); } else { this.target.bind(); - this.setQuadShift(0, 0); - gl.viewport(0, 0, width, height); - gl.scissor(0, 0, width, height); } state.disable(gl.SCISSOR_TEST); state.disable(gl.BLEND); state.disable(gl.DEPTH_TEST); state.depthMask(false); + + const { x, y, width, height } = camera.viewport; + gl.viewport(x, y, width, height); + gl.scissor(x, y, width, height); + this.renderable.render(); } + + render(camera: Camera | StereoCamera, toDrawingBuffer: boolean, props: PostprocessingProps) { + if (StereoCamera.is(camera)) { + this._render(camera.left, toDrawingBuffer, props); + this._render(camera.right, toDrawingBuffer, props); + } else { + this._render(camera, toDrawingBuffer, props); + } + } } \ No newline at end of file diff --git a/src/mol-gl/compute/util.ts b/src/mol-gl/compute/util.ts index 4d2cfee814dacd1f5859d6d02116914579fa4a6a..645d4b90146570e047a2a524ef4ea6039ee85132 100644 --- a/src/mol-gl/compute/util.ts +++ b/src/mol-gl/compute/util.ts @@ -22,7 +22,6 @@ export const QuadSchema = { instanceCount: ValueSpec('number'), aPosition: AttributeSpec('float32', 2, 0), uQuadScale: UniformSpec('v2'), - uQuadShift: UniformSpec('v2'), }; export const QuadValues: Values<typeof QuadSchema> = { @@ -30,7 +29,6 @@ export const QuadValues: Values<typeof QuadSchema> = { instanceCount: ValueCell.create(1), aPosition: ValueCell.create(QuadPositions), uQuadScale: ValueCell.create(Vec2.create(1, 1)), - uQuadShift: ValueCell.create(Vec2.create(0, 0)), }; // diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts index b21606fb4f5d42b91c563736dbb682b14de3936f..06f65477c0393f337969b1c1cc124fe58ccc1554 100644 --- a/src/mol-gl/renderable/schema.ts +++ b/src/mol-gl/renderable/schema.ts @@ -120,6 +120,7 @@ export const GlobalUniformSchema = { uViewportHeight: UniformSpec('f'), uViewport: UniformSpec('v4'), uViewOffset: UniformSpec('v2'), + uDrawingBufferSize: UniformSpec('v2'), uCameraPosition: UniformSpec('v3'), uCameraDir: UniformSpec('v3'), diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts index 32fd6aa12d11d0b14e1a38f20fd569b7364e7882..357b59fdf98c6384abc9234550daa1fcff8089c0 100644 --- a/src/mol-gl/renderer.ts +++ b/src/mol-gl/renderer.ts @@ -6,7 +6,6 @@ import { Viewport } from '../mol-canvas3d/camera/util'; import { ICamera } from '../mol-canvas3d/camera'; - import Scene from './scene'; import { WebGLContext } from './webgl/context'; import { Mat4, Vec3, Vec4, Vec2, Quat } from '../mol-math/linear-algebra'; @@ -43,7 +42,7 @@ interface Renderer { readonly props: Readonly<RendererProps> clear: (transparentBackground: boolean) => void - render: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => void + render: (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, drawingBufferScale: number, depthTexture: Texture | null) => void setProps: (props: Partial<RendererProps>) => void setViewport: (x: number, y: number, width: number, height: number) => void dispose: () => void @@ -165,6 +164,7 @@ namespace Renderer { const clip = getClip(p.clip); const viewport = Viewport(); + const drawingBufferSize = Vec2.create(gl.drawingBufferWidth, gl.drawingBufferHeight); const bgColor = Color.toVec3Normalized(Vec3(), p.backgroundColor); const view = Mat4(); @@ -195,6 +195,7 @@ namespace Renderer { uPixelRatio: ValueCell.create(ctx.pixelRatio), uViewportHeight: ValueCell.create(viewport.height), uViewport: ValueCell.create(Viewport.toVec4(Vec4(), viewport)), + uDrawingBufferSize: ValueCell.create(drawingBufferSize), uCameraPosition: ValueCell.create(Vec3()), uCameraDir: ValueCell.create(cameraDir), @@ -300,7 +301,7 @@ namespace Renderer { r.render(variant); }; - const render = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, depthTexture: Texture | null) => { + const render = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, clear: boolean, transparentBackground: boolean, drawingBufferScale: number, depthTexture: Texture | null) => { ValueCell.update(globalUniforms.uModel, group.view); ValueCell.update(globalUniforms.uView, camera.view); ValueCell.update(globalUniforms.uInvView, Mat4.invert(invView, camera.view)); @@ -311,18 +312,26 @@ namespace Renderer { ValueCell.update(globalUniforms.uModelViewProjection, Mat4.mul(modelViewProjection, modelView, camera.projection)); ValueCell.update(globalUniforms.uInvModelViewProjection, Mat4.invert(invModelViewProjection, modelViewProjection)); - ValueCell.update(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0); + ValueCell.updateIfChanged(globalUniforms.uIsOrtho, camera.state.mode === 'orthographic' ? 1 : 0); ValueCell.update(globalUniforms.uViewOffset, camera.viewOffset.enabled ? Vec2.set(viewOffset, camera.viewOffset.offsetX * 16, camera.viewOffset.offsetY * 16) : Vec2.set(viewOffset, 0, 0)); ValueCell.update(globalUniforms.uCameraPosition, camera.state.position); ValueCell.update(globalUniforms.uCameraDir, Vec3.normalize(cameraDir, Vec3.sub(cameraDir, camera.state.target, camera.state.position))); - ValueCell.update(globalUniforms.uFar, camera.far); - ValueCell.update(globalUniforms.uNear, camera.near); - ValueCell.update(globalUniforms.uFogFar, camera.fogFar); - ValueCell.update(globalUniforms.uFogNear, camera.fogNear); - - ValueCell.update(globalUniforms.uTransparentBackground, transparentBackground); + ValueCell.updateIfChanged(globalUniforms.uFar, camera.far); + ValueCell.updateIfChanged(globalUniforms.uNear, camera.near); + ValueCell.updateIfChanged(globalUniforms.uFogFar, camera.fogFar); + ValueCell.updateIfChanged(globalUniforms.uFogNear, camera.fogNear); + ValueCell.updateIfChanged(globalUniforms.uTransparentBackground, transparentBackground); + + if (gl.drawingBufferWidth * drawingBufferScale !== drawingBufferSize[0] || + gl.drawingBufferHeight * drawingBufferScale !== drawingBufferSize[1] + ) { + ValueCell.update(globalUniforms.uDrawingBufferSize, Vec2.set(drawingBufferSize, + gl.drawingBufferWidth * drawingBufferScale, + gl.drawingBufferHeight * drawingBufferScale + )); + } globalUniformsNeedUpdate = true; state.currentRenderItemId = -1; @@ -334,6 +343,10 @@ namespace Renderer { state.colorMask(true, true, true, true); state.enable(gl.DEPTH_TEST); + const { x, y, width, height } = viewport; + gl.viewport(x, y, width, height); + gl.scissor(x, y, width, height); + if (clear) { state.depthMask(true); if (variant === 'color') { diff --git a/src/mol-gl/shader/compose.frag.ts b/src/mol-gl/shader/compose.frag.ts index 9c2abd85da9a607c4bebbaa85323c0aab9f19744..172330fded2b7030fa5bd855b6e15a36c4aeeb5e 100644 --- a/src/mol-gl/shader/compose.frag.ts +++ b/src/mol-gl/shader/compose.frag.ts @@ -2,14 +2,12 @@ export default ` precision highp float; precision highp sampler2D; -uniform vec2 uQuadShift; - uniform sampler2D tColor; uniform vec2 uTexSize; uniform float uWeight; void main() { - vec2 coords = gl_FragCoord.xy / uTexSize - uQuadShift; + vec2 coords = gl_FragCoord.xy / uTexSize; gl_FragColor = texture2D(tColor, coords) * uWeight; } `; \ No newline at end of file diff --git a/src/mol-gl/shader/direct-volume.frag.ts b/src/mol-gl/shader/direct-volume.frag.ts index a3182c6c35912e22f1ca0808276eb706b4b57b7d..4da7349b10045413091b3e79a4eebf140820041b 100644 --- a/src/mol-gl/shader/direct-volume.frag.ts +++ b/src/mol-gl/shader/direct-volume.frag.ts @@ -21,7 +21,7 @@ uniform mat4 uProjection, uTransform, uModelView, uView; uniform vec3 uCameraDir; uniform sampler2D tDepth; -uniform vec4 uViewport; +uniform vec2 uDrawingBufferSize; uniform float uNear; uniform float uFar; @@ -179,7 +179,7 @@ vec4 raymarch(vec3 startLoc, vec3 step) { vec4 mvPosition = uModelView * uTransform * vec4(isoPos * uGridDim, 1.0); float depth = calcDepth(mvPosition.xyz); - if (depth > getDepth(gl_FragCoord.xy / uViewport.zw)) + if (depth > getDepth(gl_FragCoord.xy / uDrawingBufferSize)) break; #ifdef enabledFragDepth @@ -287,7 +287,7 @@ vec4 raymarch(vec3 startLoc, vec3 step) { #elif defined(dRenderMode_volume) isoPos = toUnit(pos); vec4 mvPosition = uModelView * uTransform * vec4(isoPos * uGridDim, 1.0); - if (calcDepth(mvPosition.xyz) > getDepth(gl_FragCoord.xy / uViewport.zw)) + if (calcDepth(mvPosition.xyz) > getDepth(gl_FragCoord.xy / uDrawingBufferSize)) break; vec3 vViewPosition = mvPosition.xyz; diff --git a/src/mol-gl/shader/postprocessing.frag.ts b/src/mol-gl/shader/postprocessing.frag.ts index d1558bc0969c7ff2f188e9c15d9c9a1741f3d0f5..04d2864cfa23fb87280c970c2ca764a13db04064 100644 --- a/src/mol-gl/shader/postprocessing.frag.ts +++ b/src/mol-gl/shader/postprocessing.frag.ts @@ -3,8 +3,6 @@ precision highp float; precision highp int; precision highp sampler2D; -uniform vec2 uQuadShift; - uniform sampler2D tColor; uniform sampler2D tPackedDepth; uniform vec2 uTexSize; @@ -96,7 +94,7 @@ vec2 calcEdgeDepth(const in vec2 coords) { } void main(void) { - vec2 coords = gl_FragCoord.xy / uTexSize - uQuadShift; + vec2 coords = gl_FragCoord.xy / uTexSize; vec4 color = texture2D(tColor, coords); #ifdef dOutlineEnable diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts index bc9b6e61783e59f7a5cea4a4d96e41f7e6ec9454..b2250f3a61cff551dccfba8db2bc90decadc90df 100644 --- a/src/mol-gl/webgl/context.ts +++ b/src/mol-gl/webgl/context.ts @@ -331,9 +331,9 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal state.enable(gl.SCISSOR_TEST); state.depthMask(true); state.colorMask(true, true, true, true); + state.clearColor(red, green, blue, alpha); gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); gl.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); - state.clearColor(red, green, blue, alpha); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); }, diff --git a/src/mol-gl/webgl/render-target.ts b/src/mol-gl/webgl/render-target.ts index 3209a20ff4cc4d36d27250ce997923f45ed34244..232c0d9d5388da135c61b063ad3bc0c1b396e40c 100644 --- a/src/mol-gl/webgl/render-target.ts +++ b/src/mol-gl/webgl/render-target.ts @@ -55,7 +55,6 @@ export function createRenderTarget(gl: GLRenderingContext, resources: WebGLResou getHeight: () => _height, bind: () => { framebuffer.bind(); - gl.viewport(0, 0, _width, _height); }, setSize: (width: number, height: number) => { if (_width === width && _height === height) { diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx index fe30c31eb221a8eb4eaac82595f5789bc5c15bf5..cf72e5002ca9ab640d781007e1340666d2718a7c 100644 --- a/src/mol-plugin-ui/viewport/simple-settings.tsx +++ b/src/mol-plugin-ui/viewport/simple-settings.tsx @@ -62,7 +62,6 @@ const SimpleSettingsParams = { outline: Canvas3DParams.postprocessing.params.outline, fog: Canvas3DParams.cameraFog, }, { pivot: 'renderStyle' }), - stereo: Canvas3DParams.stereo, clipping: PD.Group<any>({ ...Canvas3DParams.cameraClipping.params, ...(Canvas3DParams.renderer.params.clip as any).params as any @@ -112,7 +111,6 @@ const SimpleSettingsMapping = ParamMapping({ outline: canvas.postprocessing.outline, fog: canvas.cameraFog }, - stereo: canvas.stereo, clipping: { ...canvas.cameraClipping, ...canvas.renderer.clip @@ -138,7 +136,6 @@ const SimpleSettingsMapping = ParamMapping({ variant: s.clipping.variant, objects: s.clipping.objects, }; - canvas.stereo = s.stereo; props.layout = s.layout; }, diff --git a/src/mol-plugin/util/viewport-screenshot.ts b/src/mol-plugin/util/viewport-screenshot.ts index 677ed8b75d88912dc5fa85f60907cbb176eca7fd..bd7c43f7b94eb4cb3099ebf4fe7295feeedae830 100644 --- a/src/mol-plugin/util/viewport-screenshot.ts +++ b/src/mol-plugin/util/viewport-screenshot.ts @@ -92,7 +92,7 @@ class ViewportScreenshotHelper { this._imagePass = this.plugin.canvas3d!.getImagePass({ transparentBackground: this.transparent, - drawPass: { cameraHelper: { axes: this.axes } }, + cameraHelper: { axes: this.axes }, multiSample: { mode: 'on', sampleLevel: 2 }, postprocessing: this.plugin.canvas3d!.props.postprocessing }); @@ -140,7 +140,7 @@ class ViewportScreenshotHelper { await ctx.update('Rendering image...'); this.imagePass.setProps({ - drawPass: { cameraHelper: { axes: this.axes } }, + cameraHelper: { axes: this.axes }, transparentBackground: this.transparent, postprocessing: this.plugin.canvas3d!.props.postprocessing // TODO this line should not be required, updating should work by listening to this.plugin.events.canvas3d.settingsUpdated });