├── .gitignore ├── docs ├── bloom.jpg └── assets │ ├── ogd-oregon-360.jpg │ └── ogd-oregon-360-sm.jpg ├── blit.js ├── convolve.js ├── LICENSE.md ├── initialize-bloom.js ├── draw-env.js ├── README.md ├── package.json ├── initialize-kernel.js ├── draw-mesh.js ├── fft.js ├── composite.js ├── regl-turntable-camera.js ├── blur.js ├── fft-kernel.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /docs/bloom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rreusser/bloom-effect-example/HEAD/docs/bloom.jpg -------------------------------------------------------------------------------- /docs/assets/ogd-oregon-360.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rreusser/bloom-effect-example/HEAD/docs/assets/ogd-oregon-360.jpg -------------------------------------------------------------------------------- /docs/assets/ogd-oregon-360-sm.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rreusser/bloom-effect-example/HEAD/docs/assets/ogd-oregon-360-sm.jpg -------------------------------------------------------------------------------- /blit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (regl) { 4 | return regl({ 5 | vert: ` 6 | precision highp float; 7 | attribute vec2 xy; 8 | varying vec2 uv; 9 | void main () { 10 | uv = xy * 0.5 + 0.5; 11 | gl_Position = vec4(xy, 0, 1); 12 | } 13 | `, 14 | attributes: {xy: [-4, -4, 0, 4, 4, -4]}, 15 | depth: {enable: false}, 16 | count: 3 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /convolve.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (regl) { 4 | return regl({ 5 | frag: ` 6 | precision highp float; 7 | varying vec2 uv; 8 | uniform sampler2D src, kernel; 9 | void main () { 10 | vec4 a = texture2D(src, uv); 11 | vec4 b = texture2D(kernel, uv); 12 | gl_FragColor = vec4( 13 | a.xz * b.xz - a.yw * b.yw, 14 | a.xz * b.yw + a.yw * b.xz 15 | ).xzyw; 16 | } 17 | `, 18 | uniforms: { 19 | src: regl.prop('src'), 20 | kernel: regl.prop('kernel'), 21 | }, 22 | framebuffer: regl.prop('dst'), 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Ricky Reusser. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /initialize-bloom.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (regl) { 4 | return regl({ 5 | vert: ` 6 | precision highp float; 7 | attribute vec2 xy; 8 | varying vec2 uv; 9 | void main () { 10 | uv = xy * 0.5 + 0.5; 11 | gl_Position = vec4(xy, 0, 1); 12 | } 13 | `, 14 | frag: ` 15 | precision highp float; 16 | varying vec2 uv; 17 | uniform sampler2D src; 18 | uniform float threshold, pixelRatio; 19 | 20 | vec3 rgb2yuv (vec3 rgb) { 21 | return vec3 ( 22 | rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114, 23 | rgb.r * -0.169 + rgb.g * -0.331 + rgb.b * 0.5, 24 | rgb.r * 0.5 + rgb.g * -0.419 + rgb.b * -0.081 25 | ); 26 | } 27 | 28 | void main () { 29 | vec3 c = texture2D(src, uv).rgb; 30 | vec3 yuv = rgb2yuv(c); 31 | float strength = smoothstep(threshold - 0.02, threshold + 0.02, yuv.x); 32 | gl_FragColor = vec4(strength * c, 1); 33 | } 34 | `, 35 | uniforms: { 36 | pixelRatio: regl.context('pixelRatio'), 37 | src: regl.prop('src'), 38 | threshold: regl.prop('threshold'), 39 | }, 40 | framebuffer: regl.prop('dst'), 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /draw-env.js: -------------------------------------------------------------------------------- 1 | module.exports = function (regl) { 2 | return regl({ 3 | vert: ` 4 | precision highp float; 5 | attribute vec2 xy; 6 | uniform mat4 viewInv; 7 | varying vec3 reflectDir; 8 | uniform float aspectRatio; 9 | void main() { 10 | reflectDir = (viewInv * vec4(-xy * vec2(aspectRatio, 1), 1, 0)).xyz; 11 | gl_Position = vec4(xy, 0, 1); 12 | } 13 | `, 14 | frag: ` 15 | precision highp float; 16 | uniform sampler2D envmap; 17 | uniform float environment; 18 | varying vec3 reflectDir; 19 | #define PI ${Math.PI} 20 | 21 | vec4 lookupEnv (vec3 dir) { 22 | float lat = atan(dir.z, dir.x); 23 | float lon = acos(dir.y / length(dir)); 24 | return texture2D(envmap, vec2(0.5 + lat / (2.0 * PI), lon / PI)); 25 | } 26 | 27 | void main () { 28 | vec3 color = lookupEnv(reflectDir).rgb; 29 | color = mix(vec3(pow(0.3, 2.2)), color, environment); 30 | float power = 0.454; 31 | color.r = pow(color.r, power); 32 | color.g = pow(color.g, power); 33 | color.b = pow(color.b, power); 34 | gl_FragColor = vec4(color, 1); 35 | } 36 | `, 37 | uniforms: { 38 | envmap: regl.prop('envmap'), 39 | environment: regl.prop('environment'), 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bloom Effect Example 2 | 3 | > [regl](https://github.com/regl-project/regl)-based bloom effect in nearly-vanilla WebGL 4 | 5 | ## Introduction 6 | 7 | This example performs a bloom effect in WebGL using a [separable Gaussian blur](https://github.com/Jam3/glsl-fast-gaussian-blur). To achieve a relatively smooth blur with relatively few passes, it downsamples the full-size framebuffer when computing the blur and performs blur passes of, for example, radius 32, 16, 8, 4, 2, and finally 1 pixel. That makes it not really an exact Gaussian blur, but seems to get the job done. 8 | 9 | Please be advised that the goal here is not an optimal implementation of a bloom effect but more to present a way to implement post-processing effects like these effects in [regl](https://github.com/regl-project/regl). I hope maybe this repo is useful for someone, but don't overestimate its value. 😄 The standard if you really want to perform real-time bloom seems to be some combination of mip-maps/image pyramids and [Kawase's method](https://software.intel.com/en-us/blogs/2014/07/15/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms). 10 | 11 | Update: I've added an FFT implementation as well which performs a full convoution and uses some sort of star pattern instead of a gaussian kernel. 12 | 13 | 14 | [Live example](https://rreusser.github.io/bloom-effect-example/) 15 | 16 | ![bloom](./docs/bloom.jpg) 17 | 18 | ## License 19 | 20 | © 2020 Ricky Reusser. MIT License. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bloom", 3 | "version": "1.0.0", 4 | "description": "regl-based bloom effect in nearly-vanilla WebGL", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "budo index.js --force-default-index --open --live --dir docs", 8 | "build": "mkdirp docs && browserify index.js -t es2040 | uglifyjs -cm | indexhtmlify | html-inject-meta | html-inject-github-corner > docs/index.html" 9 | }, 10 | "keywords": [ 11 | "regl", 12 | "webgl", 13 | "bloom" 14 | ], 15 | "author": "Ricky Reusser", 16 | "license": "MIT", 17 | "dependencies": { 18 | "angle-normals": "^1.0.0", 19 | "bunny": "^1.0.1", 20 | "controls-gui": "^2.0.0", 21 | "controls-state": "^2.0.0", 22 | "fail-nicely": "^2.0.0", 23 | "gl-mat4": "^1.2.0", 24 | "inertial-turntable-camera": "^2.0.4", 25 | "is-power-of-two": "^1.0.0", 26 | "mouse-change": "^1.4.0", 27 | "mouse-wheel": "^1.2.0", 28 | "next-pow-2": "^1.0.0", 29 | "normalized-interaction-events": "^2.0.1", 30 | "regl": "^1.3.13", 31 | "resl": "^1.0.3" 32 | }, 33 | "devDependencies": { 34 | "browserify": "^16.5.0", 35 | "budo": "^11.6.3", 36 | "es2040": "^1.2.6", 37 | "html-inject-github-corner": "^2.1.4", 38 | "html-inject-meta": "^3.0.0", 39 | "indexhtmlify": "^2.0.0", 40 | "is-mobile": "^2.2.0", 41 | "mkdirp": "^1.0.3", 42 | "uglify-js": "^3.7.7" 43 | }, 44 | "github-corner": { 45 | "repository": "https://github.com/rreusser/bloom-effect-example", 46 | "bg": "#eee", 47 | "fg": "#333", 48 | "side": "left" 49 | }, 50 | "html-inject-meta": { 51 | "name": "Bloom Effect Example", 52 | "description": "regl-based bloom effect in nearly-vanilla WebGL", 53 | "url": "http://rreusser.github.io/bloom-effect-example/", 54 | "author": "Ricky Reusser", 55 | "image": "https://raw.githubusercontent.com/rreusser/bloom-effect-example/master/docs/bloom.jpg" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /initialize-kernel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (regl) { 4 | return regl({ 5 | frag: ` 6 | precision highp float; 7 | #define PI ${Math.PI} 8 | varying vec2 uv; 9 | uniform float aspectRatio, magnitude; 10 | uniform vec2 viewport; 11 | uniform float radius, points, star, power; 12 | void main () { 13 | // Do some modulo arithmetic to put (0, 0) at each of the four corners, which will place the convolution 14 | // kernel origin at each respective pixel of the image 15 | vec2 uvL = (fract(uv + 0.5) - 0.5) * vec2(aspectRatio, 1) * viewport.y; 16 | 17 | float theta = atan(uvL.y, uvL.x); 18 | float blades = pow(0.5 + 0.5 * cos(points * theta), power); 19 | 20 | float scaledRadius2 = dot(uvL / radius, uvL / radius); 21 | float scaledRadius = sqrt(scaledRadius2); 22 | 23 | float falloff = exp(-scaledRadius2); 24 | 25 | // This step is really important!! This is our convolution kernel. We convolve complex variables in a 26 | // vec4, stored as vec4(a_r, a_i, b_r, b_i). That means our convolution kernel for something like a 27 | // blur is vec4(value, 0, value, 0) and *not* vec4(value). The former is just a scalar that will keep 28 | // channels separate when we convolve it with vec4(r, g, b, a). The latter is a complex number 29 | // (value + value * i) which will rotate the phase and mix r-g and b-a, which is *not* what we want 30 | // for a simple convolution. This is easy to mix up. I do it almost every time. 31 | gl_FragColor = vec4( 32 | vec2(falloff * mix(1.0, blades, star)) * magnitude, 33 | vec2(0) 34 | ).xzyw; 35 | } 36 | `, 37 | framebuffer: regl.prop('dst'), 38 | uniforms: { 39 | viewport: ctx => [ctx.framebufferWidth, ctx.framebufferHeight], 40 | star: regl.prop('star'), 41 | points: regl.prop('points'), 42 | power: regl.prop('power'), 43 | radius: regl.prop('radius'), 44 | magnitude: regl.prop('magnitude') 45 | } 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /draw-mesh.js: -------------------------------------------------------------------------------- 1 | module.exports = function (regl, mesh) { 2 | return regl({ 3 | vert: ` 4 | precision highp float; 5 | attribute vec3 aPosition, aNormal; 6 | uniform mat4 projection, view; 7 | uniform vec3 lightPosition; 8 | varying vec3 vNormal, vPosition; 9 | uniform vec3 eye; 10 | void main () { 11 | vNormal = aNormal; 12 | vPosition = aPosition; 13 | gl_Position = projection * view * vec4(aPosition, 1); 14 | } 15 | `, 16 | frag: ` 17 | precision highp float; 18 | varying vec3 vNormal, vPosition; 19 | uniform vec3 eye, lightPosition; 20 | uniform sampler2D envmap; 21 | uniform float shininess, specular, albedo, reflectivity, environment; 22 | 23 | #define PI ${Math.PI} 24 | 25 | float blinnPhongSpecular(vec3 lightDirection, vec3 viewDirection, vec3 surfaceNormal, float shininess) { 26 | vec3 H = normalize(viewDirection + lightDirection); 27 | return pow(max(0.0, dot(surfaceNormal, H)), shininess); 28 | } 29 | 30 | vec4 lookupEnv (vec3 dir) { 31 | float lat = atan(dir.z, dir.x); 32 | float lon = acos(dir.y / length(dir)); 33 | return texture2D(envmap, vec2(0.5 + lat / (2.0 * PI), lon / PI)); 34 | } 35 | 36 | void main () { 37 | vec3 eyeDirection = normalize(eye - vPosition); 38 | vec3 lightDirection = normalize(lightPosition - vPosition); 39 | vec3 normal = normalize(vNormal); 40 | 41 | vec3 reflectDir = reflect(eyeDirection, normal); 42 | vec3 refl = lookupEnv(reflectDir).rgb * environment; 43 | refl.r = pow(refl.r, 1.0 / 2.2); 44 | refl.g = pow(refl.g, 1.0 / 2.2); 45 | refl.b = pow(refl.b, 1.0 / 2.2); 46 | 47 | vec3 color = (0.6 + 0.3 * normal) * albedo * (1.0 - reflectivity) + reflectivity * refl; 48 | 49 | float power = blinnPhongSpecular(lightDirection, eyeDirection, normal, shininess); 50 | color += specular * power; 51 | 52 | gl_FragColor = vec4(color, 1); 53 | } 54 | `, 55 | attributes: { 56 | aPosition: mesh.positions, 57 | aNormal: mesh.normals, 58 | }, 59 | uniforms: { 60 | lightPosition: [140, 130, 100], 61 | shininess: regl.prop('shininess'), 62 | albedo: regl.prop('albedo'), 63 | specular: regl.prop('specular'), 64 | reflectivity: regl.prop('reflectivity'), 65 | envmap: regl.prop('envmap'), 66 | environment: regl.prop('environment'), 67 | }, 68 | elements: mesh.cells, 69 | count: mesh.cells.length * 3, 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /fft.js: -------------------------------------------------------------------------------- 1 | var isPOT = require('is-power-of-two'); 2 | 3 | function checkPOT (label, value) { 4 | if (!isPOT(value)) { 5 | throw new Error(label + ' must be a power of two. got ' + label + ' = ' + value); 6 | } 7 | } 8 | 9 | module.exports = function (opts) { 10 | var i, ping, pong, uniforms, tmp, width, height; 11 | 12 | opts = opts || {}; 13 | opts.forward = opts.forward === undefined ? true : opts.forward; 14 | opts.splitNormalization = opts.splitNormalization === undefined ? true : opts.splitNormalization; 15 | 16 | function swap () { 17 | tmp = ping; 18 | ping = pong; 19 | pong = tmp; 20 | } 21 | 22 | if (opts.size !== undefined) { 23 | width = height = opts.size; 24 | checkPOT('size', width); 25 | } else if (opts.width !== undefined && opts.height !== undefined) { 26 | width = opts.width; 27 | height = opts.height; 28 | checkPOT('width', width); 29 | checkPOT('height', width); 30 | } else { 31 | throw new Error('either size or both width and height must provided.'); 32 | } 33 | 34 | // Swap to avoid collisions with the input: 35 | ping = opts.ping; 36 | if (opts.input === opts.pong) { 37 | ping = opts.pong; 38 | } 39 | pong = ping === opts.ping ? opts.pong : opts.ping; 40 | 41 | var passes = []; 42 | var xIterations = Math.round(Math.log(width) / Math.log(2)); 43 | var yIterations = Math.round(Math.log(height) / Math.log(2)); 44 | var iterations = xIterations + yIterations; 45 | 46 | // Swap to avoid collisions with output: 47 | if (opts.output === ((iterations % 2 === 0) ? pong : ping)) { 48 | swap(); 49 | } 50 | 51 | // If we've avoiding collision with output creates an input collision, 52 | // then you'll just have to rework your framebuffers and try again. 53 | if (opts.input === pong) { 54 | throw new Error([ 55 | 'not enough framebuffers to compute without copying data. You may perform', 56 | 'the computation with only two framebuffers, but the output must equal', 57 | 'the input when an even number of iterations are required.' 58 | ].join(' ')); 59 | } 60 | 61 | for (i = 0; i < iterations; i++) { 62 | uniforms = { 63 | src: ping, 64 | dst: pong, 65 | horizontal: i < xIterations, 66 | forward: !!opts.forward, 67 | resolution: [1.0 / width, 1.0 / height] 68 | }; 69 | 70 | if (i === 0) { 71 | uniforms.src = opts.input; 72 | } else if (i === iterations - 1) { 73 | uniforms.dst = opts.output; 74 | } 75 | 76 | if (i === 0) { 77 | if (!!opts.splitNormalization) { 78 | uniforms.normalization = 1.0 / Math.sqrt(width * height); 79 | } else if (!opts.forward) { 80 | uniforms.normalization = 1.0 / width / height; 81 | } else { 82 | uniforms.normalization = 1; 83 | } 84 | } else { 85 | uniforms.normalization = 1; 86 | } 87 | 88 | uniforms.subtransformSize = Math.pow(2, (uniforms.horizontal ? i : (i - xIterations)) + 1); 89 | 90 | passes.push(uniforms); 91 | 92 | swap(); 93 | } 94 | 95 | return passes; 96 | } 97 | -------------------------------------------------------------------------------- /composite.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (regl) { 4 | return regl({ 5 | frag: ` 6 | precision highp float; 7 | varying vec2 uv; 8 | uniform sampler2D src, bloomTex; 9 | uniform bool dither; 10 | uniform float strength; 11 | 12 | /* 13 | * https://github.com/mattdesl/glsl-random 14 | * 15 | * Copyright (c) 2014, Matt DesLauriers 16 | * 17 | * All rights reserved. 18 | * 19 | * Redistribution and use in source and binary forms, with or without modification, 20 | * are permitted provided that the following conditions are met: 21 | * 22 | * * Redistributions of source code must retain the above copyright notice, 23 | * this list of conditions and the following disclaimer. 24 | * * Redistributions in binary form must reproduce the above copyright notice, 25 | * this list of conditions and the following disclaimer in the documentation 26 | * and/or other materials provided with the distribution. 27 | * * Neither the name of glsl-random nor the names of its contributors 28 | * may be used to endorse or promote products derived from this software 29 | * without specific prior written permission. 30 | * 31 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 32 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 33 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 34 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 35 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 36 | * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 37 | * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 38 | * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 39 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 40 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 41 | * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 42 | */ 43 | highp float random(vec2 co) { 44 | highp float a = 12.9898; 45 | highp float b = 78.233; 46 | highp float c = 43758.5453; 47 | highp float dt= dot(co.xy ,vec2(a,b)); 48 | highp float sn= mod(dt,3.14); 49 | return fract(sin(sn) * c); 50 | } 51 | 52 | void main () { 53 | vec3 color = texture2D(src, uv).rgb; 54 | vec3 bloom = texture2D(bloomTex, uv).rgb; 55 | color += bloom * strength; 56 | color.r = pow(color.r, 2.2); 57 | color.g = pow(color.g, 2.2); 58 | color.b = pow(color.b, 2.2); 59 | 60 | // This doesn't seem so necessary with float/half float 61 | color += (random(gl_FragCoord.xy) - 0.5) * (dither ? 1.0 / 255.0 : 0.0); 62 | 63 | gl_FragColor = vec4(color, 1); 64 | } 65 | `, 66 | uniforms: { 67 | src: regl.prop('src'), 68 | dither: regl.prop('dither'), 69 | strength: regl.prop('strength'), 70 | bloomTex: regl.prop('bloom'), 71 | }, 72 | }); 73 | }; 74 | -------------------------------------------------------------------------------- /regl-turntable-camera.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createCamera = require('inertial-turntable-camera'); 4 | var interactionEvents = require('normalized-interaction-events'); 5 | 6 | var RADIANS_PER_HALF_SCREEN_WIDTH = Math.PI * 2 * 0.4; 7 | 8 | module.exports = function createReglCamera (regl, opts) { 9 | var element = regl._gl.canvas; 10 | 11 | function getAspectRatio () { 12 | return element.clientWidth / element.clientHeight; 13 | } 14 | 15 | var camera = createCamera(Object.assign({}, { 16 | aspectRatio: getAspectRatio(), 17 | }, opts || {})); 18 | 19 | var setCameraUniforms = regl({ 20 | context: { 21 | projection: () => camera.state.projection, 22 | view: () => camera.state.view, 23 | viewInv: () => camera.state.viewInv, 24 | eye: () => camera.state.eye, 25 | }, 26 | uniforms: { 27 | aspectRatio: () => camera.params.aspectRatio, 28 | projection: regl.context('projection'), 29 | view: regl.context('view'), 30 | viewInv: regl.context('viewInv'), 31 | eye: regl.context('eye') 32 | } 33 | }); 34 | 35 | interactionEvents(element) 36 | .on('wheel', function (ev) { 37 | camera.zoom(ev.x, ev.y, Math.exp(-ev.dy) - 1.0); 38 | ev.originalEvent.preventDefault(); 39 | }) 40 | .on('mousemove', function (ev) { 41 | if (!ev.active || ev.buttons !== 1) return; 42 | 43 | if (ev.mods.alt) { 44 | camera.zoom(ev.x0, ev.y0, Math.exp(ev.dy) - 1.0); 45 | } else if (ev.mods.shift) { 46 | camera.pan(ev.dx, ev.dy); 47 | } else if (ev.mods.meta) { 48 | camera.pivot(ev.dx, ev.dy); 49 | } else { 50 | camera.rotate( 51 | -ev.dx * RADIANS_PER_HALF_SCREEN_WIDTH, 52 | -ev.dy * RADIANS_PER_HALF_SCREEN_WIDTH 53 | ); 54 | } 55 | ev.originalEvent.preventDefault(); 56 | }) 57 | .on('touchmove', function (ev) { 58 | if (!ev.active) return; 59 | camera.rotate( 60 | -ev.dx * RADIANS_PER_HALF_SCREEN_WIDTH, 61 | -ev.dy * RADIANS_PER_HALF_SCREEN_WIDTH 62 | ); 63 | ev.originalEvent.preventDefault(); 64 | }) 65 | .on('pinchmove', function (ev) { 66 | if (!ev.active) return; 67 | camera.zoom(ev.x, ev.y, 1 - ev.zoomx); 68 | camera.pan(ev.dx, ev.dy); 69 | }) 70 | .on('touchstart', ev => ev.originalEvent.preventDefault()) 71 | .on('pinchstart', ev => ev.originalEvent.preventDefault()) 72 | 73 | 74 | function invokeCamera (props, callback) { 75 | if (!callback) { 76 | callback = props; 77 | props = {}; 78 | } 79 | 80 | camera.tick(props); 81 | 82 | setCameraUniforms(function () { 83 | callback(camera.state, camera.params); 84 | }); 85 | } 86 | 87 | invokeCamera.taint = camera.taint; 88 | invokeCamera.resize = camera.resize; 89 | invokeCamera.tick = camera.tick; 90 | invokeCamera.setUniforms = setCameraUniforms; 91 | 92 | Object.defineProperties(invokeCamera, { 93 | state: { 94 | get: function () { return camera.state; }, 95 | set: function (value) { camera.state = value; } 96 | }, 97 | params: { 98 | get: function () { return camera.params; }, 99 | set: function (value) { camera.params = value; } 100 | }, 101 | }); 102 | 103 | window.addEventListener('resize', function () { 104 | camera.resize(getAspectRatio()); 105 | }, false); 106 | 107 | return invokeCamera; 108 | }; 109 | -------------------------------------------------------------------------------- /blur.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (regl) { 4 | var blurFunctions = [ 5 | /* 6 | * https://github.com/Jam3/glsl-fast-gaussian-blur 7 | * 8 | * The MIT License (MIT) 9 | * Copyright (c) 2015 Jam3 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy 12 | * of this software and associated documentation files (the "Software"), to deal 13 | * in the Software without restriction, including without limitation the rights 14 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | * copies of the Software, and to permit persons to whom the Software is 16 | * furnished to do so, subject to the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be included in all 19 | * copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 25 | * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 26 | * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 27 | * OR OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | `vec4 blur(sampler2D image, vec2 uv, vec2 inverseResolution, vec2 direction) { 30 | vec4 color = vec4(0.0); 31 | vec2 off1 = vec2(1.3333333333333333) * direction; 32 | color += texture2D(image, uv) * 0.29411764705882354; 33 | color += texture2D(image, uv + (off1 * inverseResolution)) * 0.35294117647058826; 34 | color += texture2D(image, uv - (off1 * inverseResolution)) * 0.35294117647058826; 35 | return color; 36 | }`, 37 | `vec4 blur(sampler2D image, vec2 uv, vec2 inverseResolution, vec2 direction) { 38 | vec4 color = vec4(0.0); 39 | vec2 off1 = vec2(1.3846153846) * direction; 40 | vec2 off2 = vec2(3.2307692308) * direction; 41 | color += texture2D(image, uv) * 0.2270270270; 42 | color += texture2D(image, uv + (off1 * inverseResolution)) * 0.3162162162; 43 | color += texture2D(image, uv - (off1 * inverseResolution)) * 0.3162162162; 44 | color += texture2D(image, uv + (off2 * inverseResolution)) * 0.0702702703; 45 | color += texture2D(image, uv - (off2 * inverseResolution)) * 0.0702702703; 46 | return color; 47 | }`, 48 | `vec4 blur(sampler2D image, vec2 uv, vec2 inverseResolution, vec2 direction) { 49 | vec4 color = vec4(0.0); 50 | vec2 off1 = vec2(1.411764705882353) * direction; 51 | vec2 off2 = vec2(3.2941176470588234) * direction; 52 | vec2 off3 = vec2(5.176470588235294) * direction; 53 | color += texture2D(image, uv) * 0.1964825501511404; 54 | color += texture2D(image, uv + (off1 * inverseResolution)) * 0.2969069646728344; 55 | color += texture2D(image, uv - (off1 * inverseResolution)) * 0.2969069646728344; 56 | color += texture2D(image, uv + (off2 * inverseResolution)) * 0.09447039785044732; 57 | color += texture2D(image, uv - (off2 * inverseResolution)) * 0.09447039785044732; 58 | color += texture2D(image, uv + (off3 * inverseResolution)) * 0.010381362401148057; 59 | color += texture2D(image, uv - (off3 * inverseResolution)) * 0.010381362401148057; 60 | return color; 61 | }` 62 | ] 63 | 64 | var blurCommands = blurFunctions.map(f => regl({ 65 | frag: ` 66 | precision highp float; 67 | varying vec2 uv; 68 | uniform sampler2D src; 69 | uniform vec2 direction, inverseResolution; 70 | 71 | ${f} 72 | 73 | void main () { 74 | gl_FragColor = blur(src, uv, inverseResolution, direction); 75 | } 76 | `, 77 | uniforms: { 78 | inverseResolution: ctx => [ 79 | 1 / ctx.framebufferWidth, 80 | 1 / ctx.framebufferHeight 81 | ], 82 | src: regl.prop('src'), 83 | direction: regl.prop('direction'), 84 | }, 85 | framebuffer: regl.prop('dst'), 86 | })); 87 | 88 | return function (props) { 89 | var firstProps = Array.isArray(props) ? props[0] : props; 90 | var command = blurCommands[{5: 0, 9: 1, 13: 2}[firstProps.kernel]]; 91 | if (command === undefined) throw new Error('Blur kernel size must be 5, 9, or 13'); 92 | return command(props); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /fft-kernel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (regl) { 4 | return regl({ 5 | frag: ` 6 | // Retrieved from: https://github.com/rreusser/glsl-fft 7 | // 8 | // The MIT License (MIT) 9 | // 10 | // Copyright (c) 2017 Ricky Reusser 11 | // 12 | // Permission is hereby granted, free of charge, to any person obtaining a copy 13 | // of this software and associated documentation files (the "Software"), to deal 14 | // in the Software without restriction, including without limitation the rights 15 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | // copies of the Software, and to permit persons to whom the Software is 17 | // furnished to do so, subject to the following conditions: 18 | // 19 | // The above copyright notice and this permission notice shall be included in all 20 | // copies or substantial portions of the Software. 21 | // 22 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | // SOFTWARE. 29 | // 30 | // Original License: 31 | // 32 | // The MIT License (MIT) 33 | // 34 | // Copyright (c) 2014 David Li (http://david.li) 35 | // 36 | // Permission is hereby granted, free of charge, to any person obtaining a copy 37 | // of this software and associated documentation files (the "Software"), to deal 38 | // in the Software without restriction, including without limitation the rights 39 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | // copies of the Software, and to permit persons to whom the Software is 41 | // furnished to do so, subject to the following conditions: 42 | // 43 | // The above copyright notice and this permission notice shall be included in all 44 | // copies or substantial portions of the Software. 45 | // 46 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 52 | // SOFTWARE. 53 | precision highp float; 54 | 55 | uniform sampler2D src; 56 | uniform vec2 resolution; 57 | uniform float subtransformSize, normalization; 58 | uniform bool horizontal, forward; 59 | 60 | const float TWOPI = 6.283185307179586; 61 | 62 | vec4 fft ( 63 | sampler2D src, 64 | vec2 resolution, 65 | float subtransformSize, 66 | bool horizontal, 67 | bool forward, 68 | float normalization 69 | ) { 70 | vec2 evenPos, oddPos, twiddle, outputA, outputB; 71 | vec4 even, odd; 72 | float index, evenIndex, twiddleArgument; 73 | 74 | index = (horizontal ? gl_FragCoord.x : gl_FragCoord.y) - 0.5; 75 | 76 | evenIndex = floor(index / subtransformSize) * 77 | (subtransformSize * 0.5) + 78 | mod(index, subtransformSize * 0.5) + 79 | 0.5; 80 | 81 | if (horizontal) { 82 | evenPos = vec2(evenIndex, gl_FragCoord.y); 83 | oddPos = vec2(evenIndex, gl_FragCoord.y); 84 | } else { 85 | evenPos = vec2(gl_FragCoord.x, evenIndex); 86 | oddPos = vec2(gl_FragCoord.x, evenIndex); 87 | } 88 | 89 | evenPos *= resolution; 90 | oddPos *= resolution; 91 | 92 | if (horizontal) { 93 | oddPos.x += 0.5; 94 | } else { 95 | oddPos.y += 0.5; 96 | } 97 | 98 | even = texture2D(src, evenPos); 99 | odd = texture2D(src, oddPos); 100 | 101 | twiddleArgument = (forward ? TWOPI : -TWOPI) * (index / subtransformSize); 102 | twiddle = vec2(cos(twiddleArgument), sin(twiddleArgument)); 103 | 104 | return (even.rgba + vec4( 105 | twiddle.x * odd.xz - twiddle.y * odd.yw, 106 | twiddle.y * odd.xz + twiddle.x * odd.yw 107 | ).xzyw) * normalization; 108 | } 109 | 110 | void main () { 111 | gl_FragColor = fft(src, resolution, subtransformSize, horizontal, forward, normalization); 112 | } 113 | `, 114 | uniforms: { 115 | src: regl.prop('src'), 116 | resolution: regl.prop('resolution'), 117 | forward: regl.prop('forward'), 118 | subtransformSize: regl.prop('subtransformSize'), 119 | horizontal: regl.prop('horizontal'), 120 | normalization: regl.prop('normalization'), 121 | }, 122 | framebuffer: regl.prop('dst'), 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Controls = require('controls-state'); 4 | var Gui = require('controls-gui'); 5 | var angleNormals = require('angle-normals'); 6 | var createCamera = require('./regl-turntable-camera'); 7 | var isMobile = require('is-mobile')() 8 | var nextPow2 = require('next-pow-2'); 9 | 10 | var pixelRatio = Math.min(window.devicePixelRatio, 2.0); 11 | 12 | require('resl')({ 13 | manifest: { 14 | envmap: { 15 | type: 'image', 16 | src: 'assets/ogd-oregon-360.jpg', 17 | } 18 | }, 19 | onDone: assets => { 20 | require('regl')({ 21 | pixelRatio: pixelRatio, 22 | extensions: [ 23 | 'OES_texture_float', 24 | 'OES_texture_float_linear', 25 | ], 26 | optionalExtensions: [ 27 | 'OES_texture_half_float', 28 | 'OES_texture_half_float_linear', 29 | ], 30 | attributes: { 31 | antialias: false 32 | }, 33 | onDone: require('fail-nicely')(regl => run(regl, assets)) 34 | }); 35 | } 36 | }) 37 | 38 | function run (regl, assets) { 39 | regl._gl.canvas.style.position = 'fixed' 40 | var hasHalfFloat = regl.hasExtension('OES_texture_half_float') && regl.hasExtension('OES_texture_half_float_linear'); 41 | var envmap = regl.texture({ 42 | data: assets.envmap, 43 | min: 'linear', 44 | mag: 'linear', 45 | flipY: true 46 | }); 47 | 48 | var bunny = require('bunny'); 49 | bunny.normals = angleNormals(bunny.cells, bunny.positions); 50 | 51 | var camera = createCamera(regl, { 52 | center: [0, 4, 0], 53 | theta: 0.4, 54 | phi: 0.1, 55 | damping: 0, 56 | distance: 20, 57 | noScroll: true, 58 | renderOnDirty: true, 59 | }); 60 | 61 | var initialBlurSize = Math.round(window.innerHeight / 40) 62 | var needsNewKernel = true; 63 | 64 | var state = Gui(Controls({ 65 | material: Controls.Section({ 66 | shininess: Controls.Slider(2000.0, { mapping: x => Math.pow(10, x), inverseMapping: Math.log10, min: 1, max: 10000, steps: 4 * 10}), 67 | specular: Controls.Slider(2.0, { min: 0, max: 5, step: 0.01 }), 68 | reflectivity: Controls.Slider(0.5, { min: 0, max: 1, step: 0.01 }), 69 | albedo: Controls.Slider(1.0, { min: 0, max: 1, step: 0.01 }), 70 | environment: Controls.Slider(1.0, { min: 0, max: 1, step: 0.01 }), 71 | }, {expanded: !isMobile}), 72 | bloom: Controls.Section({ 73 | strength: Controls.Slider(2.0, { min: 0, max: 20, step: 0.1 }), 74 | radius: Controls.Slider(initialBlurSize, { mapping: x => Math.pow(2, x), inverseMapping: Math.log2, min: 1, max: 64, steps: 12 * 2 }).onChange(() => needsNewKernel = true), 75 | threshold: Controls.Slider(2.0, { min: 0, max: 10, step: 0.01 }), 76 | downsample: Controls.Slider(1, { mapping: x => Math.pow(2, x), inverseMapping: Math.log2, min: 1, max: 16, steps: 4 }), 77 | method: Controls.Select('FFT convolution', {options: [ 78 | 'Gaussian blur', 79 | 'FFT convolution' 80 | ]}), 81 | dither: true, 82 | blur: Controls.Section({ 83 | passes: Controls.Slider(1, {min: 1, max: 4, step: 1}), 84 | kernelSize: Controls.Select(13, {options: [5, 9, 13]}), 85 | }, { 86 | label: 'Gaussian blur parameters' 87 | }), 88 | convolution: Controls.Section({ 89 | star: Controls.Slider(0.7, {min: 0, max: 1, step: 0.01}), 90 | points: Controls.Slider(10, {min: 2, max: 24, step: 1}), 91 | power: Controls.Slider(2.0, {min: 0, max: 4, step: 0.01}), 92 | }, { 93 | label: 'Convolution kernel parameters' 94 | }).onChange(() => needsNewKernel = true), 95 | }, {expanded: !isMobile}) 96 | }), { 97 | containerCSS: `position: absolute; top: 0; right: 0;` 98 | }); 99 | 100 | // Redraw when config or window size change 101 | state.$onChange(camera.taint); 102 | window.addEventListener('resize', () => { 103 | needsNewKernel = true; 104 | camera.taint() 105 | }); 106 | 107 | // Create a framebuffer to which to draw the scene 108 | var fbo = regl.framebuffer({ 109 | radius: 1, 110 | colorType: hasHalfFloat ? 'half float' : 'float', 111 | }); 112 | 113 | // Create two ping-pong framebuffers for blurring the bloom 114 | var bloomFbo = [0, 1, 2, 3].map(() => regl.framebuffer({ 115 | color: regl.texture({ 116 | radius: 1, 117 | type: hasHalfFloat ? 'half float' : 'float', 118 | mag: 'linear' 119 | }) 120 | })); 121 | 122 | var kernel = regl.framebuffer({ 123 | color: regl.texture({ 124 | radius: 1, 125 | type: hasHalfFloat ? 'half float' : 'float', 126 | mag: 'nearest' 127 | }) 128 | }); 129 | 130 | var kernelFFT = regl.framebuffer({ 131 | color: regl.texture({ 132 | radius: 1, 133 | type: hasHalfFloat ? 'half float' : 'float', 134 | mag: 'nearest' 135 | }) 136 | }); 137 | 138 | // Create a command to draw the mesh. Since we're only drawing one mesh, we'll just pass it 139 | // the data and let regl create the buffers rather than managing them ourselves. 140 | var drawMesh = require('./draw-mesh')(regl, bunny); 141 | 142 | var drawEnv = require('./draw-env')(regl); 143 | 144 | var planFFT = require('./fft'); 145 | var fftKernel = require('./fft-kernel')(regl); 146 | var initializeKernel = require('./initialize-kernel')(regl); 147 | var convolve = require('./convolve')(regl); 148 | var plannedFFTWidth, plannedFFTHeight; 149 | var forwardFFTPlan, inverseFFTPlan, kernelFFTPlan; 150 | 151 | // Create a shader which sets up a single fullscreen triangle which we'll use to wrap the 152 | // subsequent shaders so they are configured to draw all fragments. 153 | var blit = require('./blit')(regl); 154 | 155 | // Create a command to compute the initial un-blurred bloom with the scene fbo as input. 156 | // This could be avoided with some clever overloading of shaders or with webgl draw buffers 157 | // to write to this *while* drawing the scene. 158 | var initializeBloom = require('./initialize-bloom')(regl); 159 | 160 | // Create a command for a single pass of a blur kernel 161 | var blur = require('./blur')(regl); 162 | 163 | // Finally a command to composite the fbo and bloom to the screen 164 | var composite = require('./composite')(regl); 165 | 166 | var loop = regl.frame(({tick, viewportWidth, viewportHeight, pixelRatio}) => { 167 | try { 168 | // Resize the framebuffers to match the window size. If the size hasn't changed these 169 | // are no-ops. 170 | fbo.resize(viewportWidth, viewportHeight); 171 | 172 | if (state.bloom.method === 'FFT convolution') { 173 | var fftWidth = nextPow2(viewportWidth / state.bloom.downsample) / 2; 174 | var fftHeight = nextPow2(viewportHeight / state.bloom.downsample) / 2; 175 | 176 | bloomFbo.forEach(fbo => fbo.resize(fftWidth, fftHeight)); 177 | kernel.resize(fftWidth, fftHeight); 178 | kernelFFT.resize(fftWidth, fftHeight); 179 | 180 | if (plannedFFTWidth !== fftWidth || plannedFFTHeight !== fftHeight || needsNewKernel) { 181 | plannedFFTWidth = fftWidth; 182 | plannedFFTHeight = fftHeight; 183 | 184 | forwardFFTPlan = planFFT({ 185 | width: plannedFFTWidth, 186 | height: plannedFFTHeight, 187 | input: bloomFbo[0], 188 | ping: bloomFbo[1], 189 | pong: bloomFbo[2], 190 | output: bloomFbo[0], 191 | splitNormalization: true, 192 | forward: true 193 | }); 194 | 195 | inverseFFTPlan = planFFT({ 196 | width: plannedFFTWidth, 197 | height: plannedFFTHeight, 198 | input: bloomFbo[3], 199 | ping: bloomFbo[1], 200 | pong: bloomFbo[2], 201 | output: bloomFbo[0], 202 | splitNormalization: true, 203 | forward: false 204 | }); 205 | 206 | kernelFFTPlan = planFFT({ 207 | width: plannedFFTWidth, 208 | height: plannedFFTHeight, 209 | input: kernel, 210 | ping: bloomFbo[1], 211 | pong: bloomFbo[2], 212 | output: kernelFFT, 213 | splitNormalization: true, 214 | forward: true 215 | }); 216 | 217 | blit(() => { 218 | camera(() => { 219 | initializeKernel({ 220 | dst: kernel, 221 | points: state.bloom.convolution.points, 222 | power: state.bloom.convolution.power, 223 | star: state.bloom.convolution.star, 224 | radius: state.bloom.radius / state.bloom.downsample * 2.0, 225 | magnitude: 1.0 / Math.pow(state.bloom.downsample, 0.5) 226 | }); 227 | }); 228 | fftKernel(kernelFFTPlan); 229 | }); 230 | needsNewKernel = false; 231 | } 232 | } else { 233 | // Downsample the bloom framebuffer as necessary to save computation for what's heavily 234 | // blurred anyway. 235 | bloomFbo.forEach(fbo => fbo.resize( 236 | Math.floor(viewportWidth / state.bloom.downsample), 237 | Math.floor(viewportHeight / state.bloom.downsample) 238 | )); 239 | } 240 | 241 | camera(() => { 242 | // Draw the mesh to the "fbo" framebuffer 243 | fbo.use(() => { 244 | regl.clear({color: [0.3, 0.3, 0.3, 1.0], depth: 1}); 245 | if (state.material.environment) { 246 | blit(() => { 247 | drawEnv({ 248 | envmap: envmap, 249 | environment: state.material.environment, 250 | }); 251 | }); 252 | } 253 | drawMesh(Object.assign({}, state.material, {envmap})); 254 | }); 255 | 256 | // Configure a full-screen triangle 257 | blit(() => { 258 | // Threshold the fbo and initialize the bloom 259 | initializeBloom({ 260 | src: fbo, 261 | dst: bloomFbo[0], 262 | threshold: state.bloom.threshold 263 | }); 264 | 265 | if (state.bloom.method === 'FFT convolution') { 266 | fftKernel(forwardFFTPlan); 267 | 268 | convolve({ 269 | src: bloomFbo[0], 270 | kernel: kernelFFT, 271 | dst: bloomFbo[3] 272 | }); 273 | 274 | fftKernel(inverseFFTPlan); 275 | 276 | } else { 277 | // Construct the blur passes 278 | var passes = []; 279 | var radii = [Math.round(Math.max(1, state.bloom.radius * pixelRatio / state.bloom.downsample))]; 280 | for (var radius = nextPow2(radii[0]) / 2; radius >= 1; radius /= 2) { 281 | radii.push(radius); 282 | } 283 | radii.forEach(radius => { 284 | for (var pass = 0; pass < state.bloom.blur.passes; pass++) { 285 | passes.push({ 286 | kernel: state.bloom.blur.kernelSize, 287 | src: bloomFbo[0], 288 | dst: bloomFbo[1], 289 | direction: [radius, 0] 290 | }, { 291 | kernel: state.bloom.blur.kernelSize, 292 | src: bloomFbo[1], 293 | dst: bloomFbo[0], 294 | direction: [0, radius] 295 | }); 296 | } 297 | }); 298 | 299 | blur(passes); 300 | } 301 | 302 | // Composite everything to the screen! 303 | composite({ 304 | src: fbo, 305 | bloom: bloomFbo[0], 306 | dither: state.bloom.dither, 307 | strength: state.bloom.strength 308 | }); 309 | }); 310 | }); 311 | } catch (e) { 312 | loop.cancel(); 313 | console.error(e); 314 | } 315 | }); 316 | } 317 | --------------------------------------------------------------------------------