Browse Source

add fxaa option to postprocessing pass

Alexander Rose 4 years ago
parent
commit
468e14bc35

+ 1 - 1
src/mol-canvas3d/passes/draw.ts

@@ -192,7 +192,7 @@ export class DrawPass {
         }
 
         // do direct-volume rendering
-        if (!toDrawingBuffer && scene.volumes.renderables.length > 0) {
+        if (!toDrawingBuffer) {
             if (!this.packedDepth) {
                 this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
                 renderer.clearDepth(); // from previous frame

+ 92 - 25
src/mol-canvas3d/passes/postprocessing.ts

@@ -17,9 +17,9 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
 import { DrawPass } from './draw';
 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 fxaa_frag from '../../mol-gl/shader/fxaa.frag';
 import { StereoCamera } from '../camera/stereo';
 
 const PostprocessingSchema = {
@@ -92,7 +92,8 @@ export const PostprocessingParams = {
             threshold: PD.Numeric(0.8, { min: 0, max: 5, step: 0.01 }),
         }),
         off: PD.Group({})
-    }, { cycle: true, description: 'Draw outline around 3D objects' })
+    }, { cycle: true, description: 'Draw outline around 3D objects' }),
+    antialiasing: PD.Boolean(true, { description: 'Fast Approximate Anti-Aliasing (FXAA)' })
 };
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
@@ -101,21 +102,21 @@ export class PostprocessingPass {
         return props.occlusion.name === 'on' || props.outline.name === 'on';
     }
 
-    target: RenderTarget
-    renderable: PostprocessingRenderable
+    readonly target: RenderTarget
+
+    private readonly tmpTarget: RenderTarget
+    private readonly renderable: PostprocessingRenderable
+    private readonly fxaa: FxaaRenderable
 
     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);
-    }
+        const width = colorTarget.getWidth();
+        const height = colorTarget.getHeight();
 
-    private bindTarget(toDrawingBuffer: boolean) {
-        if (toDrawingBuffer) {
-            this.webgl.unbindFramebuffer();
-        } else {
-            this.target.bind();
-        }
+        this.target = webgl.createRenderTarget(width, height, false);
+        this.tmpTarget = webgl.createRenderTarget(width, height, false);
+        this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture);
+        this.fxaa = getFxaaRenderable(webgl, this.tmpTarget.texture);
     }
 
     syncSize() {
@@ -125,11 +126,29 @@ export class PostprocessingPass {
         const [w, h] = this.renderable.values.uTexSize.ref.value;
         if (width !== w || height !== h) {
             this.target.setSize(width, height);
+            this.tmpTarget.setSize(width, height);
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.fxaa.values.uTexSize, Vec2.set(this.fxaa.values.uTexSize.ref.value, width, height));
         }
     }
 
-    _render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+    private updateState(camera: ICamera) {
+        const { gl, state } = this.webgl;
+
+        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);
+
+        state.clearColor(0, 0, 0, 1);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+    }
+
+    private _renderPostprocessing(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
         const { values } = this.renderable;
 
         ValueCell.updateIfChanged(values.uFar, camera.far);
@@ -166,19 +185,44 @@ export class PostprocessingPass {
             this.renderable.update();
         }
 
-        const { gl, state } = this.webgl;
-        this.bindTarget(toDrawingBuffer);
+        if (props.antialiasing) {
+            this.tmpTarget.bind();
+        } else if (toDrawingBuffer) {
+            this.webgl.unbindFramebuffer();
+        } else {
+            this.target.bind();
+        }
 
-        state.disable(gl.SCISSOR_TEST);
-        state.disable(gl.BLEND);
-        state.disable(gl.DEPTH_TEST);
-        state.depthMask(false);
+        this.updateState(camera);
+        this.renderable.render();
+    }
 
-        const { x, y, width, height } = camera.viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+    private _renderFxaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        const input = (props.occlusion.name === 'on' || props.outline.name === 'on')
+            ? this.tmpTarget.texture : this.drawPass.colorTarget.texture;
+        if (this.fxaa.values.tColor.ref.value !== input) {
+            ValueCell.update(this.fxaa.values.tColor, input);
+            this.fxaa.update();
+        }
 
-        this.renderable.render();
+        if (toDrawingBuffer) {
+            this.webgl.unbindFramebuffer();
+        } else {
+            this.target.bind();
+        }
+
+        this.updateState(camera);
+        this.fxaa.render();
+    }
+
+    private _render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
+        if (props.occlusion.name === 'on' || props.outline.name === 'on' || !props.antialiasing) {
+            this._renderPostprocessing(camera, toDrawingBuffer, props);
+        }
+
+        if (props.antialiasing) {
+            this._renderFxaa(camera, toDrawingBuffer, props);
+        }
     }
 
     render(camera: Camera | StereoCamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
@@ -189,4 +233,27 @@ export class PostprocessingPass {
             this._render(camera, toDrawingBuffer, props);
         }
     }
-}
+}
+
+//
+
+const FxaaSchema = {
+    ...QuadSchema,
+    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+};
+const FxaaShaderCode = ShaderCode('fxaa', quad_vert, fxaa_frag);
+type FxaaRenderable = ComputeRenderable<Values<typeof FxaaSchema>>
+
+function getFxaaRenderable(ctx: WebGLContext, colorTexture: Texture): FxaaRenderable {
+    const values: Values<typeof FxaaSchema> = {
+        ...QuadValues,
+        tColor: ValueCell.create(colorTexture),
+        uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
+    };
+
+    const schema = { ...FxaaSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', FxaaShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}

+ 107 - 0
src/mol-gl/shader/fxaa.frag.ts

@@ -0,0 +1,107 @@
+export default `
+precision highp float;
+precision highp int;
+precision highp sampler2D;
+
+uniform sampler2D tColor;
+uniform vec2 uTexSize;
+
+// Basic FXAA implementation based on the code on geeks3d.com with the
+// modification that the texture2DLod stuff was removed since it's
+// unsupported by WebGL.
+// --
+// From:
+// https://github.com/mitsuhiko/webgl-meincraft
+// Copyright (c) 2011 by Armin Ronacher.
+// Some rights reserved.
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//     * Redistributions of source code must retain the above copyright
+//       notice, this list of conditions and the following disclaimer.
+//     * Redistributions in binary form must reproduce the above
+//       copyright notice, this list of conditions and the following
+//       disclaimer in the documentation and/or other materials provided
+//       with the distribution.
+//     * The names of the contributors may not be used to endorse or
+//       promote products derived from this software without specific
+//       prior written permission.
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+// */
+
+#ifndef FXAA_REDUCE_MIN
+    #define FXAA_REDUCE_MIN   (1.0/ 128.0)
+#endif
+#ifndef FXAA_REDUCE_MUL
+    #define FXAA_REDUCE_MUL   (1.0 / 8.0)
+#endif
+#ifndef FXAA_SPAN_MAX
+    #define FXAA_SPAN_MAX     8.0
+#endif
+
+vec4 fxaa(sampler2D tex, const in vec2 fragCoord, const in vec2 resolution) {
+    vec2 inverseVP = 1.0 / resolution;
+    vec2 v_rgbNW = (fragCoord + vec2(-1.0, -1.0)) * inverseVP;
+    vec2 v_rgbNE = (fragCoord + vec2(1.0, -1.0)) * inverseVP;
+    vec2 v_rgbSW = (fragCoord + vec2(-1.0, 1.0)) * inverseVP;
+    vec2 v_rgbSE = (fragCoord + vec2(1.0, 1.0)) * inverseVP;
+    vec2 v_rgbM = vec2(fragCoord * inverseVP);
+
+    vec4 col = vec4(0.0);
+    vec3 rgbNW = texture2D(tex, v_rgbNW).xyz;
+    vec3 rgbNE = texture2D(tex, v_rgbNE).xyz;
+    vec3 rgbSW = texture2D(tex, v_rgbSW).xyz;
+    vec3 rgbSE = texture2D(tex, v_rgbSE).xyz;
+    vec4 texColor = texture2D(tex, v_rgbM);
+    vec3 rgbM  = texColor.xyz;
+    vec3 luma = vec3(0.299, 0.587, 0.114);
+    float lumaNW = dot(rgbNW, luma);
+    float lumaNE = dot(rgbNE, luma);
+    float lumaSW = dot(rgbSW, luma);
+    float lumaSE = dot(rgbSE, luma);
+    float lumaM  = dot(rgbM,  luma);
+    float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
+    float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
+
+    vec2 dir;
+    dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
+    dir.y =  ((lumaNW + lumaSW) - (lumaNE + lumaSE));
+
+    float dirReduce = max((lumaNW + lumaNE + lumaSW + lumaSE) *
+                          (0.25 * FXAA_REDUCE_MUL), FXAA_REDUCE_MIN);
+
+    float rcpDirMin = 1.0 / (min(abs(dir.x), abs(dir.y)) + dirReduce);
+    dir = min(vec2(FXAA_SPAN_MAX, FXAA_SPAN_MAX),
+              max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),
+              dir * rcpDirMin)) * inverseVP;
+
+    vec4 rgbA1 = texture2D(tex, fragCoord * inverseVP + dir * (1.0 / 3.0 - 0.5));
+    vec4 rgbA2 = texture2D(tex, fragCoord * inverseVP + dir * (2.0 / 3.0 - 0.5));
+    vec4 rgbA = 0.5 * (rgbA1 + rgbA2);
+
+    vec4 rgbB1 = texture2D(tex, fragCoord * inverseVP + dir * -0.5);
+    vec4 rgbB2 = texture2D(tex, fragCoord * inverseVP + dir * 0.5);
+    vec4 rgbB = rgbA * 0.5 + 0.25 * (rgbB1 + rgbB2);
+
+    float lumaB = dot(rgbB.rgb, luma);
+    if ((lumaB < lumaMin) || (lumaB > lumaMax))
+        col = vec4(rgbA.rgb, rgbA.a);
+    else
+        col = vec4(rgbB.rgb, rgbB.a);
+    return col;
+}
+
+void main(void) {
+    gl_FragColor = fxaa(tColor, gl_FragCoord.xy, uTexSize);
+}
+`;

+ 8 - 2
src/mol-plugin/util/viewport-screenshot.ts

@@ -115,7 +115,10 @@ class ViewportScreenshotHelper extends PluginComponent {
                 mode: mutlisample ? 'on' : 'off',
                 sampleLevel: colorBufferFloat && textureFloat ? 4 : 2
             },
-            postprocessing: c.props.postprocessing
+            postprocessing: {
+                ...c.props.postprocessing,
+                antialiasing: false
+            }
         });
     }
 
@@ -131,7 +134,10 @@ class ViewportScreenshotHelper extends PluginComponent {
                 cameraHelper: { axes: this.values.axes },
                 transparentBackground: this.values.transparent,
                 // TODO: optimize because this creates a copy of a large object!
-                postprocessing: this.plugin.canvas3d!.props.postprocessing
+                postprocessing: {
+                    ...this.plugin.canvas3d!.props.postprocessing,
+                    antialiasing: false
+                }
             });
             return this._imagePass;
         }