Browse Source

add mipmap-based blur for image backgrounds

Alexander Rose 1 year ago
parent
commit
609e03f7d2

+ 1 - 0
CHANGELOG.md

@@ -9,6 +9,7 @@ Note that since we don't clearly distinguish between a public and private interf
 - Add `inverted` option to `xrayShaded` parameter
 - Model-export extension: Add ability to set a file name for structures
 - Add `contextHash` to `SizeTheme`
+- Add mipmap-based blur for image backgrounds
 
 ## [v3.36.1] - 2023-06-11
 

+ 1 - 0
src/extensions/backgrounds/index.ts

@@ -50,6 +50,7 @@ export const Backgrounds = PluginBehavior.create<{ }>({
                             lightness: 0,
                             saturation: 0,
                             opacity: 1,
+                            blur: 0,
                             coverage: 'viewport',
                         }
                     }

+ 7 - 1
src/mol-canvas3d/passes/background.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -23,6 +23,7 @@ import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
 import { Color } from '../../mol-util/color';
 import { Asset, AssetManager } from '../../mol-util/assets';
 import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
+import { isPowerOfTwo } from '../../mol-math/misc';
 
 const SharedParams = {
     opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }),
@@ -59,6 +60,7 @@ const ImageParams = {
         url: PD.Text(''),
         file: PD.File({ accept: 'image/*' }),
     }),
+    blur: PD.Numeric(0, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'Note, this only works in WebGL2 or with power-of-two images and when "EXT_shader_texture_lod" is available.' }),
     ...SharedParams,
     coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
 };
@@ -207,6 +209,7 @@ export class BackgroundPass {
         }
         if (!this.image) return;
 
+        ValueCell.updateIfChanged(this.renderable.values.uBlur, props.blur);
         ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity);
         ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation);
         ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness);
@@ -394,6 +397,9 @@ function getImageTexture(ctx: WebGLContext, assetManager: AssetManager, source:
     const img = new Image();
     img.onload = () => {
         texture.load(img);
+        if (ctx.isWebGL2 || (isPowerOfTwo(img.width) && isPowerOfTwo(img.height))) {
+            texture.mipmap();
+        }
         onload?.();
     };
     img.onerror = () => {

+ 6 - 1
src/mol-gl/shader/background.frag.ts

@@ -14,6 +14,7 @@ precision mediump sampler2D;
     uniform sampler2D tImage;
     uniform vec2 uImageScale;
     uniform vec2 uImageOffset;
+    uniform float uBlur;
     uniform float uOpacity;
     uniform float uSaturation;
     uniform float uLightness;
@@ -64,7 +65,11 @@ void main() {
         } else {
             coords = (gl_FragCoord.xy / uImageScale) + uImageOffset;
         }
-        gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y));
+        #ifdef enabledShaderTextureLod
+            gl_FragColor = texture2DLodEXT(tImage, vec2(coords.x, 1.0 - coords.y), uBlur * 8.0);
+        #else
+            gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y));
+        #endif
         gl_FragColor.a = uOpacity;
         gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness);
     #elif defined(dVariant_horizontalGradient)

+ 24 - 1
src/mol-gl/webgl/texture.ts

@@ -15,6 +15,7 @@ import { isWebGL2, GLRenderingContext } from './compat';
 import { isPromiseLike, ValueOf } from '../../mol-util/type-helpers';
 import { WebGLExtensions } from './extensions';
 import { objectForEach } from '../../mol-util/object';
+import { isPowerOfTwo } from '../../mol-math/misc';
 
 const getNextTextureId = idFactory();
 
@@ -214,6 +215,7 @@ export interface Texture {
      * `define` or `load` without `sub` must have been called before.
      */
     load: (image: TextureImage<any> | TextureVolume<any> | HTMLImageElement, sub?: boolean) => void
+    mipmap: () => void
     bind: (id: TextureId) => void
     unbind: (id: TextureId) => void
     /** Use `layer` to attach a z-slice of a 3D texture */
@@ -275,6 +277,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
 
     let width = 0, height = 0, depth = 0;
     let loadedData: undefined | TextureImage<any> | TextureVolume<any> | HTMLImageElement;
+    let hasMipmap = false;
     let destroyed = false;
 
     function define(_width: number, _height: number, _depth?: number) {
@@ -337,6 +340,22 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
         loadedData = data;
     }
 
+    function mipmap() {
+        if (target !== gl.TEXTURE_2D) {
+            throw new Error('mipmap only supported for 2d textures');
+        }
+
+        if (isWebGL2(gl) || (isPowerOfTwo(width) && isPowerOfTwo(height))) {
+            gl.bindTexture(target, texture);
+            gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
+            gl.generateMipmap(target);
+            gl.bindTexture(target, null);
+            hasMipmap = true;
+        } else {
+            throw new Error('mipmap unsupported for non-power-of-two textures and webgl1');
+        }
+    }
+
     function attachFramebuffer(framebuffer: Framebuffer, attachment: TextureAttachment, layer?: number) {
         framebuffer.bind();
         if (target === gl.TEXTURE_2D) {
@@ -365,6 +384,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
 
         define,
         load,
+        mipmap,
         bind: (id: TextureId) => {
             gl.activeTexture(gl.TEXTURE0 + id);
             gl.bindTexture(target, texture);
@@ -392,6 +412,7 @@ export function createTexture(gl: GLRenderingContext, extensions: WebGLExtension
             width = 0, height = 0, depth = 0; // set to zero to trigger resize
             define(_width, _height, _depth);
             if (loadedData) load(loadedData);
+            if (hasMipmap) mipmap();
         },
         destroy: () => {
             if (destroyed) return;
@@ -498,8 +519,8 @@ export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, mipm
             if (loadedCount === 6) {
                 if (!destroyed) {
                     if (mipmaps) {
-                        gl.generateMipmap(target);
                         gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
+                        gl.generateMipmap(target);
                     } else {
                         gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter);
                     }
@@ -532,6 +553,7 @@ export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, mipm
 
         define: () => {},
         load: () => {},
+        mipmap: () => {},
         bind: (id: TextureId) => {
             gl.activeTexture(gl.TEXTURE0 + id);
             gl.bindTexture(target, texture);
@@ -577,6 +599,7 @@ export function createNullTexture(gl?: GLRenderingContext): Texture {
 
         define: () => {},
         load: () => {},
+        mipmap: () => {},
         bind: (id: TextureId) => {
             if (gl) {
                 gl.activeTexture(gl.TEXTURE0 + id);