From 0e46b0b2ec7ce70227ae1eadda00a8eb37214773 Mon Sep 17 00:00:00 2001 From: Alexander Rose <alexander.rose@weirdbyte.de> Date: Mon, 13 May 2019 12:01:17 -0700 Subject: [PATCH] multisample antialiasing --- src/mol-canvas3d/canvas3d.ts | 141 ++++++++++++++++++++---- src/mol-canvas3d/helper/multi-sample.ts | 80 ++++++++++++++ src/mol-gl/shader/compose.frag | 11 ++ 3 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 src/mol-canvas3d/helper/multi-sample.ts create mode 100644 src/mol-gl/shader/compose.frag diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts index f1a359800..e380a6142 100644 --- a/src/mol-canvas3d/canvas3d.ts +++ b/src/mol-canvas3d/canvas3d.ts @@ -32,6 +32,7 @@ import { Canvas3dInteractionHelper } from './helper/interaction-events'; import { createTexture } from 'mol-gl/webgl/texture'; import { ValueCell } from 'mol-util'; import { getPostprocessingRenderable, PostprocessingParams } from './helper/postprocessing'; +import { JitterVectors, getComposeRenderable } from './helper/multi-sample'; export const Canvas3DParams = { // TODO: FPS cap? @@ -41,6 +42,8 @@ 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 }), + sampleLevel: PD.Numeric(0, { min: 0, max: 5, step: 1 }), + postprocessing: PD.Group(PostprocessingParams), renderer: PD.Group(RendererParams), trackball: PD.Group(TrackballControlsParams), @@ -107,16 +110,17 @@ namespace Canvas3D { mode: p.cameraMode }) - const gl = getGLContext(canvas, { + const _gl = getGLContext(canvas, { alpha: false, antialias: true, depth: true, preserveDrawingBuffer: true }) - if (gl === null) { + if (_gl === null) { throw new Error('Could not create a WebGL rendering context') } - const webgl = createContext(gl) + const webgl = createContext(_gl) + const { state, gl } = webgl const scene = Scene.create(webgl) const controls = TrackballControls.create(input, camera, p.trackball) @@ -126,9 +130,15 @@ namespace Canvas3D { const depthTexture = createTexture(webgl, 'image-depth', 'depth', 'ushort', 'nearest') depthTexture.define(canvas.width, canvas.height) depthTexture.attachFramebuffer(drawTarget.framebuffer, 'depth') + + const postprocessingTarget = createRenderTarget(webgl, canvas.width, canvas.height) const postprocessing = getPostprocessingRenderable(webgl, drawTarget.texture, depthTexture, p.postprocessing) - let pickScale = 0.25 / webgl.pixelRatio + const composeTarget = createRenderTarget(webgl, canvas.width, canvas.height) + const compose = getComposeRenderable(webgl, drawTarget.texture) + + const pickBaseScale = 0.25 + let pickScale = pickBaseScale / webgl.pixelRatio let pickWidth = Math.round(canvas.width * pickScale) let pickHeight = Math.round(canvas.height * pickScale) const objectPickTarget = createRenderTarget(webgl, pickWidth, pickHeight) @@ -189,9 +199,9 @@ namespace Canvas3D { let fogFar = cDist + (bRadius * fogFarFactor) if (camera.state.mode === 'perspective') { - near = Math.max(0.1, p.cameraClipDistance, near) + near = Math.max(1, p.cameraClipDistance, near) far = Math.max(1, far) - fogNear = Math.max(0.1, fogNear) + fogNear = Math.max(1, fogNear) fogFar = Math.max(1, fogFar) } else if (camera.state.mode === 'orthographic') { if (p.cameraClipDistance > 0) { @@ -205,6 +215,92 @@ namespace Canvas3D { } } + function renderDraw() { + renderer.setViewport(0, 0, canvas.width, canvas.height) + renderer.render(scene, 'draw') + if (debugHelper.isEnabled) { + debugHelper.syncVisibility() + renderer.render(debugHelper.scene, 'draw') + } + } + + function renderPostprocessing() { + gl.viewport(0, 0, canvas.width, canvas.height) + state.disable(gl.SCISSOR_TEST) + state.disable(gl.BLEND) + state.disable(gl.DEPTH_TEST) + state.depthMask(false) + postprocessing.render() + } + + function renderMultiSample(postprocessingEnabled: boolean) { + // 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 ones 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, postprocessingEnabled ? postprocessingTarget.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 (postprocessingEnabled) { + postprocessingTarget.bind() + renderPostprocessing() + } + + // compose draw with hold target + composeTarget.bind() + gl.viewport(0, 0, canvas.width, canvas.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, canvas.width, canvas.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 @@ -228,22 +324,16 @@ namespace Canvas3D { break; case 'draw': renderer.setViewport(0, 0, canvas.width, canvas.height); - if (postprocessingEnabled) { - drawTarget.bind() + if (p.sampleLevel > 0) { + renderMultiSample(postprocessingEnabled) } else { - webgl.unbindFramebuffer(); - } - renderer.render(scene, 'draw'); - if (debugHelper.isEnabled) { - debugHelper.syncVisibility() - renderer.render(debugHelper.scene, 'draw') - } - if (postprocessingEnabled) { - webgl.unbindFramebuffer(); - webgl.state.disable(webgl.gl.SCISSOR_TEST) - webgl.state.disable(webgl.gl.BLEND) - webgl.state.disable(webgl.gl.DEPTH_TEST) - postprocessing.render() + if (postprocessingEnabled) drawTarget.bind() + else webgl.unbindFramebuffer() + renderDraw() + if (postprocessingEnabled) { + webgl.unbindFramebuffer() + renderPostprocessing() + } } pickDirty = true break; @@ -418,6 +508,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.sampleLevel !== undefined) p.sampleLevel = props.sampleLevel + if (props.postprocessing) { if (props.postprocessing.occlusionEnable !== undefined) { p.postprocessing.occlusionEnable = props.postprocessing.occlusionEnable @@ -465,6 +557,8 @@ namespace Canvas3D { clip: p.clip, fog: p.fog, + sampleLevel: p.sampleLevel, + postprocessing: { ...p.postprocessing }, renderer: { ...renderer.props }, trackball: { ...controls.props }, @@ -498,10 +592,13 @@ namespace Canvas3D { Viewport.set(controls.viewport, 0, 0, canvas.width, canvas.height) drawTarget.setSize(canvas.width, canvas.height) + postprocessingTarget.setSize(canvas.width, canvas.height) + composeTarget.setSize(canvas.width, canvas.height) depthTexture.define(canvas.width, canvas.height) ValueCell.update(postprocessing.values.uTexSize, Vec2.set(postprocessing.values.uTexSize.ref.value, canvas.width, canvas.height)) + ValueCell.update(compose.values.uTexSize, Vec2.set(compose.values.uTexSize.ref.value, canvas.width, canvas.height)) - pickScale = 0.25 / webgl.pixelRatio + pickScale = pickBaseScale / webgl.pixelRatio pickWidth = Math.round(canvas.width * pickScale) pickHeight = Math.round(canvas.height * pickScale) objectPickTarget.setSize(pickWidth, pickHeight) diff --git a/src/mol-canvas3d/helper/multi-sample.ts b/src/mol-canvas3d/helper/multi-sample.ts new file mode 100644 index 000000000..418205b10 --- /dev/null +++ b/src/mol-canvas3d/helper/multi-sample.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + */ + +import { QuadSchema, QuadValues } from 'mol-gl/compute/util'; +import { TextureSpec, UniformSpec, Values } from 'mol-gl/renderable/schema'; +import { Texture } from 'mol-gl/webgl/texture'; +import { WebGLContext } from 'mol-gl/webgl/context'; +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'; + +const ComposeSchema = { + ...QuadSchema, + tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'), + uTexSize: UniformSpec('v2'), + uWeight: UniformSpec('f'), +} + +export function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture) { + const values: Values<typeof ComposeSchema> = { + ...QuadValues, + tColor: ValueCell.create(colorTexture), + uTexSize: ValueCell.create(Vec2.create(colorTexture.width, colorTexture.height)), + uWeight: ValueCell.create(1.0), + } + + const schema = { ...ComposeSchema } + const shaderCode = ShaderCode( + require('mol-gl/shader/quad.vert').default, + require('mol-gl/shader/compose.frag').default + ) + const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values) + + return createComputeRenderable(renderItem, values) +} + +export const JitterVectors = [ + [ + [ 0, 0 ] + ], + [ + [ 4, 4 ], [ -4, -4 ] + ], + [ + [ -2, -6 ], [ 6, -2 ], [ -6, 2 ], [ 2, 6 ] + ], + [ + [ 1, -3 ], [ -1, 3 ], [ 5, 1 ], [ -3, -5 ], + [ -5, 5 ], [ -7, -1 ], [ 3, 7 ], [ 7, -7 ] + ], + [ + [ 1, 1 ], [ -1, -3 ], [ -3, 2 ], [ 4, -1 ], + [ -5, -2 ], [ 2, 5 ], [ 5, 3 ], [ 3, -5 ], + [ -2, 6 ], [ 0, -7 ], [ -4, -6 ], [ -6, 4 ], + [ -8, 0 ], [ 7, -4 ], [ 6, 7 ], [ -7, -8 ] + ], + [ + [ -4, -7 ], [ -7, -5 ], [ -3, -5 ], [ -5, -4 ], + [ -1, -4 ], [ -2, -2 ], [ -6, -1 ], [ -4, 0 ], + [ -7, 1 ], [ -1, 2 ], [ -6, 3 ], [ -3, 3 ], + [ -7, 6 ], [ -3, 6 ], [ -5, 7 ], [ -1, 7 ], + [ 5, -7 ], [ 1, -6 ], [ 6, -5 ], [ 4, -4 ], + [ 2, -3 ], [ 7, -2 ], [ 1, -1 ], [ 4, -1 ], + [ 2, 1 ], [ 6, 2 ], [ 0, 4 ], [ 4, 4 ], + [ 2, 5 ], [ 7, 5 ], [ 5, 6 ], [ 3, 7 ] + ] +] + +JitterVectors.forEach(offsetList => { + offsetList.forEach(offset => { + // 0.0625 = 1 / 16 + offset[0] *= 0.0625 + offset[1] *= 0.0625 + }) +}) \ No newline at end of file diff --git a/src/mol-gl/shader/compose.frag b/src/mol-gl/shader/compose.frag new file mode 100644 index 000000000..ddc876820 --- /dev/null +++ b/src/mol-gl/shader/compose.frag @@ -0,0 +1,11 @@ +precision highp float; +precision highp sampler2D; + +uniform sampler2D tColor; +uniform vec2 uTexSize; +uniform float uWeight; + +void main() { + vec2 coords = gl_FragCoord.xy / uTexSize; + gl_FragColor = texture2D(tColor, coords) * uWeight; +} \ No newline at end of file -- GitLab