diff --git a/src/apps/image-generator/index.ts b/src/apps/image-generator/index.ts index a4cba746ca064f6eb2fdf7beb79649554e90fcec..6224bc0317f07ccd82cbf1401bdce4b92d242c0c 100644 --- a/src/apps/image-generator/index.ts +++ b/src/apps/image-generator/index.ts @@ -30,8 +30,10 @@ const gl = createContext(width, height, { const input = InputObserver.create() const canvas3d = Canvas3D.create(gl, input, { - multiSample: 'on', - sampleLevel: 3, + multiSample: { + mode: 'on', + sampleLevel: 3 + }, renderer: { ...Canvas3DParams.renderer.defaultValue, lightIntensity: 0, diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index 83bd67d0a4813e6a73592e7100d1f9df02efb4a6..0d9d56c3220f7ce4292e4d6472aa7c16f3a571fb 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -6,12 +6,10 @@ import { BehaviorSubject, Subscription } from 'rxjs'; import { now } from 'mol-util/now'; - -import { Vec3, Vec2 } from 'mol-math/linear-algebra' +import { Vec3 } from 'mol-math/linear-algebra' import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer' import Renderer, { RendererStats, RendererParams } from 'mol-gl/renderer' import { GraphicsRenderObject } from 'mol-gl/render-object' - import { TrackballControls, TrackballControlsParams } from './controls/trackball' import { Viewport } from './camera/util' import { createContext, WebGLContext, getGLContext } from 'mol-gl/webgl/context'; @@ -29,9 +27,8 @@ import { decodeFloatRGB } from 'mol-util/float-packing'; import { SetUtils } from 'mol-util/set'; import { Canvas3dInteractionHelper } from './helper/interaction-events'; import { createTexture } from 'mol-gl/webgl/texture'; -import { ValueCell } from 'mol-util'; import { PostprocessingParams, PostprocessingPass } from './helper/postprocessing'; -import { JitterVectors, getComposeRenderable } from './helper/multi-sample'; +import { MultiSampleParams, MultiSamplePass } from './helper/multi-sample'; import { GLRenderingContext } from 'mol-gl/webgl/compat'; import { PixelData } from 'mol-util/image'; import { readTexture } from 'mol-gl/compute/util'; @@ -44,9 +41,7 @@ export const Canvas3DParams = { clip: PD.Interval([1, 100], { min: 1, max: 100, step: 1 }), fog: PD.Interval([50, 100], { min: 1, max: 100, step: 1 }), - multiSample: PD.Select('off', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]), - sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }), - + multiSample: PD.Group(MultiSampleParams), postprocessing: PD.Group(PostprocessingParams), renderer: PD.Group(RendererParams), trackball: PD.Group(TrackballControlsParams), @@ -127,7 +122,6 @@ namespace Canvas3D { }) const webgl = createContext(gl) - const { state } = webgl let width = gl.drawingBufferWidth let height = gl.drawingBufferHeight @@ -145,10 +139,7 @@ namespace Canvas3D { } const postprocessing = new PostprocessingPass(webgl, drawTarget.texture, depthTexture, !!depthTarget, p.postprocessing) - - const composeTarget = createRenderTarget(webgl, width, height) - const holdTarget = createRenderTarget(webgl, width, height) - const compose = getComposeRenderable(webgl, drawTarget.texture) + const multiSample = new MultiSamplePass(webgl, camera, drawTarget, postprocessing, renderDraw, p.multiSample) const pickBaseScale = 0.5 let pickScale = pickBaseScale / webgl.pixelRatio @@ -163,9 +154,6 @@ namespace Canvas3D { let isUpdating = false let drawPending = false - let multiSampleIndex = -1 - let multiSample = false - const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug); const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input); @@ -249,166 +237,6 @@ namespace Canvas3D { } } - function renderTemporalMultiSample() { - // based on the Multisample Anti-Aliasing Render Pass - // contributed to three.js by bhouston / http://clara.io/ - // - // 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(p.sampleLevel, 5)) ] - - if (multiSampleIndex === -1) return - if (multiSampleIndex >= offsetList.length) { - multiSampleIndex = -1 - return - } - - const i = multiSampleIndex - - if (i === 0) { - drawTarget.bind() - renderDraw() - if (postprocessing.enabled) postprocessing.render(false) - ValueCell.update(compose.values.uWeight, 1.0) - ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture) - compose.update() - - holdTarget.bind() - state.disable(gl.BLEND) - compose.render() - } - - const sampleWeight = 1.0 / offsetList.length - - camera.viewOffset.enabled = true - ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture) - ValueCell.update(compose.values.uWeight, sampleWeight) - compose.update() - - const { width, height } = drawTarget - - // render the scene multiple times, each slightly jitter offset - // from the last and accumulate the results. - const numSamplesPerFrame = Math.pow(2, p.sampleLevel) - for (let i = 0; i < numSamplesPerFrame; ++i) { - const offset = offsetList[multiSampleIndex] - Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height) - camera.updateMatrices() - - // render scene and optionally postprocess - drawTarget.bind() - renderDraw() - if (postprocessing.enabled) postprocessing.render(false) - - // compose rendered scene with compose target - composeTarget.bind() - state.enable(gl.BLEND) - 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) - if (multiSampleIndex === 0) { - webgl.state.clearColor(0, 0, 0, 0) - gl.clear(gl.COLOR_BUFFER_BIT) - } - compose.render() - - multiSampleIndex += 1 - if (multiSampleIndex >= offsetList.length ) break - } - - const accumulationWeight = multiSampleIndex * sampleWeight - if (accumulationWeight > 0) { - ValueCell.update(compose.values.uWeight, 1.0) - ValueCell.update(compose.values.tColor, composeTarget.texture) - compose.update() - webgl.unbindFramebuffer() - gl.viewport(0, 0, width, height) - state.disable(gl.BLEND) - compose.render() - } - if (accumulationWeight < 1.0) { - ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight) - ValueCell.update(compose.values.tColor, holdTarget.texture) - compose.update() - webgl.unbindFramebuffer() - gl.viewport(0, 0, width, height) - if (accumulationWeight === 0) state.disable(gl.BLEND) - else state.enable(gl.BLEND) - compose.render() - } - - camera.viewOffset.enabled = false - camera.updateMatrices() - if (multiSampleIndex >= offsetList.length) multiSampleIndex = -1 - } - - function renderMultiSample() { - // based on the Multisample Anti-Aliasing Render Pass - // contributed to three.js by bhouston / http://clara.io/ - // - // 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(p.sampleLevel, 5)) ] - - const baseSampleWeight = 1.0 / offsetList.length - const roundingRange = 1 / 32 - - camera.viewOffset.enabled = true - ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture) - compose.update() - - const { width, height } = drawTarget - - // 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.updateMatrices() - - // 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 - // distributed across a range of values whose rounding errors cancel each other out. - const uniformCenteredDistribution = -0.5 + (i + 0.5) / offsetList.length - const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution - ValueCell.update(compose.values.uWeight, sampleWeight) - - // render scene and optionally postprocess - drawTarget.bind() - renderDraw() - if (postprocessing.enabled) postprocessing.render(false) - - // compose rendered scene with compose target - composeTarget.bind() - gl.viewport(0, 0, width, height) - state.enable(gl.BLEND) - 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) - if (i === 0) { - webgl.state.clearColor(0, 0, 0, 0) - gl.clear(gl.COLOR_BUFFER_BIT) - } - compose.render() - } - - ValueCell.update(compose.values.uWeight, 1.0) - ValueCell.update(compose.values.tColor, composeTarget.texture) - compose.update() - - webgl.unbindFramebuffer() - gl.viewport(0, 0, width, height) - state.disable(gl.BLEND) - compose.render() - - camera.viewOffset.enabled = false - camera.updateMatrices() - } - function render(variant: 'pick' | 'draw', force: boolean) { if (isIdentifying || isUpdating) return false @@ -417,21 +245,9 @@ namespace Canvas3D { // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane. if (!camera.transition.inTransition) setClipping(); const cameraChanged = camera.updateMatrices(); - if (force || cameraChanged) lastRenderTime = now() - if (p.multiSample === 'temporal') { - if (currentTime - lastRenderTime > 200) { - multiSample = multiSampleIndex !== -1 - } else { - multiSampleIndex = 0 - multiSample = false - } - } else if (p.multiSample === 'on') { - multiSample = true - } else { - multiSample = false - } + multiSample.update(force || cameraChanged, currentTime) - if (force || cameraChanged || multiSample) { + if (force || cameraChanged || multiSample.enabled) { switch (variant) { case 'pick': renderer.setViewport(0, 0, pickWidth, pickHeight); @@ -444,12 +260,8 @@ namespace Canvas3D { break; case 'draw': renderer.setViewport(0, 0, width, height); - if (multiSample) { - if (p.multiSample === 'temporal') { - renderTemporalMultiSample() - } else { - renderMultiSample() - } + if (multiSample.enabled) { + multiSample.render() } else { if (postprocessing.enabled) drawTarget.bind() else webgl.unbindFramebuffer() @@ -467,7 +279,6 @@ namespace Canvas3D { let forceNextDraw = false; let currentTime = 0; - let lastRenderTime = 0; function draw(force?: boolean) { if (render('draw', !!force || forceNextDraw)) { @@ -636,11 +447,8 @@ namespace Canvas3D { if (props.clip !== undefined) p.clip = [props.clip[0], props.clip[1]] if (props.fog !== undefined) p.fog = [props.fog[0], props.fog[1]] - if (props.multiSample !== undefined) p.multiSample = props.multiSample - if (props.sampleLevel !== undefined) p.sampleLevel = props.sampleLevel - if (props.postprocessing) postprocessing.setProps(props.postprocessing) - + if (props.multiSample) multiSample.setProps(props.multiSample) if (props.renderer) renderer.setProps(props.renderer) if (props.trackball) controls.setProps(props.trackball) if (props.debug) debugHelper.setProps(props.debug) @@ -654,10 +462,8 @@ namespace Canvas3D { clip: p.clip, fog: p.fog, - multiSample: p.multiSample, - sampleLevel: p.sampleLevel, - postprocessing: { ...postprocessing.props }, + multiSample: { ...multiSample.props }, renderer: { ...renderer.props }, trackball: { ...controls.props }, debug: { ...debugHelper.props } @@ -693,14 +499,13 @@ namespace Canvas3D { drawTarget.setSize(width, height) postprocessing.setSize(width, height) - composeTarget.setSize(width, height) - holdTarget.setSize(width, height) + multiSample.setSize(width, height) + if (depthTarget) { depthTarget.setSize(width, height) } else { depthTexture.define(width, height) } - ValueCell.update(compose.values.uTexSize, Vec2.set(compose.values.uTexSize.ref.value, width, height)) pickScale = pickBaseScale / webgl.pixelRatio pickWidth = Math.round(width * pickScale) diff --git a/src/mol-canvas3d/helper/multi-sample.ts b/src/mol-canvas3d/helper/multi-sample.ts index 3cec89814b6e94162525b7cbbd8168a8ba361b06..d2d9647dc5226df4010cc6bdd714a0d1386ce126 100644 --- a/src/mol-canvas3d/helper/multi-sample.ts +++ b/src/mol-canvas3d/helper/multi-sample.ts @@ -12,7 +12,11 @@ import { ValueCell } from 'mol-util'; import { Vec2 } from 'mol-math/linear-algebra'; import { ShaderCode } from 'mol-gl/shader-code'; import { createComputeRenderItem } from 'mol-gl/webgl/render-item'; -import { createComputeRenderable } from 'mol-gl/renderable'; +import { createComputeRenderable, ComputeRenderable } from 'mol-gl/renderable'; +import { ParamDefinition as PD } from 'mol-util/param-definition'; +import { RenderTarget, createRenderTarget } from 'mol-gl/webgl/render-target'; +import { Camera } from 'mol-canvas3d/camera'; +import { PostprocessingPass } from './postprocessing'; const ComposeSchema = { ...QuadSchema, @@ -21,7 +25,9 @@ const ComposeSchema = { uWeight: UniformSpec('f'), } -export function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture) { +type ComposeRenderable = ComputeRenderable<Values<typeof ComposeSchema>> + +function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): ComposeRenderable { const values: Values<typeof ComposeSchema> = { ...QuadValues, tColor: ValueCell.create(colorTexture), @@ -39,7 +45,238 @@ export function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture) { return createComputeRenderable(renderItem, values) } -export const JitterVectors = [ +export const MultiSampleParams = { + mode: PD.Select('off', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]), + sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }), +} +export type MultiSampleProps = PD.Values<typeof MultiSampleParams> + +export class MultiSamplePass { + props: MultiSampleProps + + private composeTarget: RenderTarget + private holdTarget: RenderTarget + private compose: ComposeRenderable + + private sampleIndex = -1 + private currentTime = 0 + private lastRenderTime = 0 + + constructor(private webgl: WebGLContext, private camera: Camera, private drawTarget: RenderTarget, private postprocessing: PostprocessingPass, private renderDraw: () => void, props: Partial<MultiSampleProps>) { + const { gl } = webgl + this.composeTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight) + this.holdTarget = createRenderTarget(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight) + this.compose = getComposeRenderable(webgl, drawTarget.texture) + this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props } + } + + get enabled() { + if (this.props.mode === 'temporal') { + if (this.currentTime - this.lastRenderTime > 200) { + return this.sampleIndex !== -1 + } else { + this.sampleIndex = 0 + return false + } + } else if (this.props.mode === 'on') { + return true + } else { + return false + } + } + + update(changed: boolean, currentTime: number) { + if (changed) this.lastRenderTime = currentTime + this.currentTime = currentTime + } + + setSize(width: number, height: number) { + this.composeTarget.setSize(width, height) + this.holdTarget.setSize(width, height) + ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height)) + } + + setProps(props: Partial<MultiSampleProps>) { + if (props.mode !== undefined) this.props.mode = props.mode + if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel + } + + render() { + if (this.props.mode === 'temporal') { + this.renderTemporalMultiSample() + } else { + this.renderMultiSample() + } + } + + private renderMultiSample() { + const { camera, compose, drawTarget, composeTarget, postprocessing, renderDraw, webgl } = this + const { gl, state } = webgl + + // based on the Multisample Anti-Aliasing Render Pass + // contributed to three.js by bhouston / http://clara.io/ + // + // 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 baseSampleWeight = 1.0 / offsetList.length + const roundingRange = 1 / 32 + + camera.viewOffset.enabled = true + ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture) + compose.update() + + const { width, height } = drawTarget + + // 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.updateMatrices() + + // 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 + // distributed across a range of values whose rounding errors cancel each other out. + const uniformCenteredDistribution = -0.5 + (i + 0.5) / offsetList.length + const sampleWeight = baseSampleWeight + roundingRange * uniformCenteredDistribution + ValueCell.update(compose.values.uWeight, sampleWeight) + + // render scene and optionally postprocess + drawTarget.bind() + renderDraw() + if (postprocessing.enabled) postprocessing.render(false) + + // compose rendered scene with compose target + composeTarget.bind() + gl.viewport(0, 0, width, height) + state.enable(gl.BLEND) + 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) + if (i === 0) { + state.clearColor(0, 0, 0, 0) + gl.clear(gl.COLOR_BUFFER_BIT) + } + compose.render() + } + + ValueCell.update(compose.values.uWeight, 1.0) + ValueCell.update(compose.values.tColor, composeTarget.texture) + compose.update() + + webgl.unbindFramebuffer() + gl.viewport(0, 0, width, height) + state.disable(gl.BLEND) + compose.render() + + camera.viewOffset.enabled = false + camera.updateMatrices() + } + + private renderTemporalMultiSample() { + const { camera, compose, drawTarget, composeTarget, holdTarget, postprocessing, renderDraw, webgl } = this + const { gl, state } = webgl + + // based on the Multisample Anti-Aliasing Render Pass + // contributed to three.js by bhouston / http://clara.io/ + // + // 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)) ] + + if (this.sampleIndex === -1) return + if (this.sampleIndex >= offsetList.length) { + this.sampleIndex = -1 + return + } + + const i = this.sampleIndex + + if (i === 0) { + drawTarget.bind() + renderDraw() + if (postprocessing.enabled) postprocessing.render(false) + ValueCell.update(compose.values.uWeight, 1.0) + ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture) + compose.update() + + holdTarget.bind() + state.disable(gl.BLEND) + compose.render() + } + + const sampleWeight = 1.0 / offsetList.length + + camera.viewOffset.enabled = true + ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawTarget.texture) + ValueCell.update(compose.values.uWeight, sampleWeight) + compose.update() + + const { width, height } = drawTarget + + // render the scene multiple times, each slightly jitter offset + // from the last and accumulate the results. + const numSamplesPerFrame = Math.pow(2, this.props.sampleLevel) + 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.updateMatrices() + + // render scene and optionally postprocess + drawTarget.bind() + renderDraw() + if (postprocessing.enabled) postprocessing.render(false) + + // compose rendered scene with compose target + composeTarget.bind() + state.enable(gl.BLEND) + 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) + if (this.sampleIndex === 0) { + state.clearColor(0, 0, 0, 0) + gl.clear(gl.COLOR_BUFFER_BIT) + } + compose.render() + + this.sampleIndex += 1 + if (this.sampleIndex >= offsetList.length ) break + } + + const accumulationWeight = this.sampleIndex * sampleWeight + if (accumulationWeight > 0) { + ValueCell.update(compose.values.uWeight, 1.0) + ValueCell.update(compose.values.tColor, composeTarget.texture) + compose.update() + webgl.unbindFramebuffer() + gl.viewport(0, 0, width, height) + state.disable(gl.BLEND) + compose.render() + } + if (accumulationWeight < 1.0) { + ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight) + ValueCell.update(compose.values.tColor, holdTarget.texture) + compose.update() + webgl.unbindFramebuffer() + gl.viewport(0, 0, width, height) + if (accumulationWeight === 0) state.disable(gl.BLEND) + else state.enable(gl.BLEND) + compose.render() + } + + camera.viewOffset.enabled = false + camera.updateMatrices() + if (this.sampleIndex >= offsetList.length) this.sampleIndex = -1 + } +} + +const JitterVectors = [ [ [ 0, 0 ] ], diff --git a/src/mol-canvas3d/helper/postprocessing.ts b/src/mol-canvas3d/helper/postprocessing.ts index 39fc3965988e334f2e17d7716e4303202e14220f..eb54c3a424bf155d04c4ec611139d098f1a8232a 100644 --- a/src/mol-canvas3d/helper/postprocessing.ts +++ b/src/mol-canvas3d/helper/postprocessing.ts @@ -48,7 +48,7 @@ export type PostprocessingProps = PD.Values<typeof PostprocessingParams> type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>> -export function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, props: Partial<PostprocessingProps>): PostprocessingRenderable { +function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, props: Partial<PostprocessingProps>): PostprocessingRenderable { const p = { ...PD.getDefaultValues(PostprocessingParams), ...props } const values: Values<typeof PostprocessingSchema> = { ...QuadValues,