Browse Source

multisample antialiasing

Alexander Rose 5 years ago
parent
commit
0e46b0b2ec
3 changed files with 210 additions and 22 deletions
  1. 119 22
      src/mol-canvas3d/canvas3d.ts
  2. 80 0
      src/mol-canvas3d/helper/multi-sample.ts
  3. 11 0
      src/mol-gl/shader/compose.frag

+ 119 - 22
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)

+ 80 - 0
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
+    })
+})

+ 11 - 0
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;
+}