├── .npmignore ├── docs ├── mist.jpg └── index.html ├── example ├── example.js ├── index.css └── index.js ├── wavenumber.glsl ├── index.glsl ├── package.json ├── LICENSE ├── index.js ├── README.md └── test └── test.js /.npmignore: -------------------------------------------------------------------------------- 1 | example 2 | docs 3 | test 4 | -------------------------------------------------------------------------------- /docs/mist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rreusser/glsl-fft/HEAD/docs/mist.jpg -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | var fft = require('../'); 2 | 3 | console.log(fft({ 4 | width: 4, 5 | height: 2, 6 | input: 'a', 7 | ping: 'b', 8 | pong: 'c', 9 | output: 'd', 10 | forward: true, 11 | normalization: 'inverse' 12 | })); 13 | -------------------------------------------------------------------------------- /example/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | } 4 | 5 | #root { 6 | display: inline-block; 7 | width: 512px; 8 | height: 512px; 9 | } 10 | 11 | input[type=range] { 12 | width: 400px; 13 | } 14 | 15 | .readout { 16 | width: 50px; 17 | display: inline-block; 18 | } 19 | -------------------------------------------------------------------------------- /wavenumber.glsl: -------------------------------------------------------------------------------- 1 | const float TWOPI = 3.14159265358979 * 2.0; 2 | 3 | vec2 wavenumber (vec2 resolution) { 4 | vec2 xy = (gl_FragCoord.xy - 0.5) * resolution; 5 | 6 | return vec2( 7 | (xy.x < 0.5) ? xy.x : xy.x - 1.0, 8 | (xy.y < 0.5) ? xy.y : xy.y - 1.0 9 | ) * TWOPI; 10 | } 11 | 12 | vec2 wavenumber (vec2 resolution, float dx) { 13 | vec2 xy = (gl_FragCoord.xy - 0.5) * resolution; 14 | 15 | return vec2( 16 | (xy.x < 0.5) ? xy.x : xy.x - 1.0, 17 | (xy.y < 0.5) ? xy.y : xy.y - 1.0 18 | ) * (TWOPI / dx); 19 | } 20 | 21 | vec2 wavenumber (vec2 resolution, vec2 dxy) { 22 | vec2 xy = (gl_FragCoord.xy - 0.5) * resolution; 23 | 24 | return vec2( 25 | (xy.x < 0.5) ? xy.x : xy.x - 1.0, 26 | (xy.y < 0.5) ? xy.y : xy.y - 1.0 27 | ) * TWOPI / dxy; 28 | } 29 | 30 | #pragma glslify: export(wavenumber) 31 | -------------------------------------------------------------------------------- /index.glsl: -------------------------------------------------------------------------------- 1 | const float TWOPI = 6.283185307179586; 2 | 3 | vec4 fft ( 4 | sampler2D src, 5 | vec2 resolution, 6 | float subtransformSize, 7 | bool horizontal, 8 | bool forward, 9 | float normalization 10 | ) { 11 | vec2 evenPos, oddPos, twiddle, outputA, outputB; 12 | vec4 even, odd; 13 | float index, evenIndex, twiddleArgument; 14 | 15 | index = (horizontal ? gl_FragCoord.x : gl_FragCoord.y) - 0.5; 16 | 17 | evenIndex = floor(index / subtransformSize) * 18 | (subtransformSize * 0.5) + 19 | mod(index, subtransformSize * 0.5) + 20 | 0.5; 21 | 22 | if (horizontal) { 23 | evenPos = vec2(evenIndex, gl_FragCoord.y); 24 | oddPos = vec2(evenIndex, gl_FragCoord.y); 25 | } else { 26 | evenPos = vec2(gl_FragCoord.x, evenIndex); 27 | oddPos = vec2(gl_FragCoord.x, evenIndex); 28 | } 29 | 30 | evenPos *= resolution; 31 | oddPos *= resolution; 32 | 33 | if (horizontal) { 34 | oddPos.x += 0.5; 35 | } else { 36 | oddPos.y += 0.5; 37 | } 38 | 39 | even = texture2D(src, evenPos); 40 | odd = texture2D(src, oddPos); 41 | 42 | twiddleArgument = (forward ? TWOPI : -TWOPI) * (index / subtransformSize); 43 | twiddle = vec2(cos(twiddleArgument), sin(twiddleArgument)); 44 | 45 | return (even.rgba + vec4( 46 | twiddle.x * odd.xz - twiddle.y * odd.yw, 47 | twiddle.y * odd.xz + twiddle.x * odd.yw 48 | ).xzyw) * normalization; 49 | } 50 | 51 | #pragma glslify: export(fft) 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "glsl-fft", 3 | "version": "1.0.3", 4 | "description": "GLSL setup for performing a Fast Fourier Transform of two complex matrices", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "start": "budo example/index.js --force-default-index --dir docs --open --host localhost --live -- -t glslify -t es2040 -t brfs", 11 | "build": "browserify example/index.js -t glslify -t es2040 -t brfs -t [ envify --NODE_ENV production ] | uglifyjs -cm | indexhtmlify | html-inject-meta | html-inject-github-corner > docs/index.html", 12 | "deps": "dependency-check package.json", 13 | "test": "npm run deps && node test/test.js", 14 | "test-browser": "browserify test/*.js | testling -x open" 15 | }, 16 | "keywords": [ 17 | "webgl", 18 | "glsl", 19 | "glslify", 20 | "fft", 21 | "fourier", 22 | "transform", 23 | "spectrum" 24 | ], 25 | "author": "Ricky Reusser", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "almost-equal": "^1.1.0", 29 | "brfs": "^1.4.3", 30 | "budo": "^10.0.4", 31 | "dependency-check": "^2.9.1", 32 | "envify": "^4.1.0", 33 | "es2040": "^1.2.6", 34 | "fail-nicely": "^2.0.0", 35 | "glsl-noise": "0.0.0", 36 | "glslify": "^6.1.0", 37 | "h": "^0.1.0", 38 | "html-inject-github-corner": "^2.0.0", 39 | "html-inject-meta": "^3.0.0", 40 | "indexhtmlify": "^1.3.1", 41 | "insert-css": "^2.0.0", 42 | "iota-array": "^1.0.0", 43 | "is-mobile": "^0.2.2", 44 | "ndarray": "^1.0.18", 45 | "ndarray-fft": "^1.0.3", 46 | "ndarray-scratch": "^1.2.0", 47 | "ndarray-show": "^2.0.0", 48 | "phantomjs": "^2.1.7", 49 | "regl": "^1.3.0", 50 | "resl": "^1.0.3", 51 | "tap-spec": "^4.1.1", 52 | "testling": "^1.7.1", 53 | "uglify-js": "^3.1.6" 54 | }, 55 | "browserify": { 56 | "transform": [ 57 | "glslify" 58 | ] 59 | }, 60 | "github-corner": { 61 | "url": "https://github.com/rreusser/glsl-fft" 62 | }, 63 | "dependencies": { 64 | "is-power-of-two": "^1.0.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Ricky Reusser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Original License: 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2014 David Li (http://david.li) 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. 46 | -------------------------------------------------------------------------------- /index.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 | input: ping, 64 | output: 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.input = opts.input; 72 | } else if (i === iterations - 1) { 73 | uniforms.output = 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 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const h = require('h'); 2 | const fs = require('fs'); 3 | const fft = require('../'); 4 | const css = require('insert-css'); 5 | const path = require('path'); 6 | const resl = require('resl'); 7 | const regl = require('regl'); 8 | const glsl = require('glslify'); 9 | const mobile = require('is-mobile'); 10 | 11 | const radiusSlider = h('input', {type: 'range', min: 0, max: 50, step: 0.1, id: 'radius', value: 10}); 12 | const angleSlider = h('input', {type: 'range', min: 0, max: 180, step: 1, id: 'angle', value: 0}); 13 | const radiusReadout = h('span', {class: 'readout'}); 14 | const angleReadout = h('span', {class: 'readout'}); 15 | const controls = h('div', [ 16 | h('div', [h('label', 'Radius:', {for: 'radius'}), radiusSlider, radiusReadout]), 17 | h('div', [h('label', 'Angle:', {for: 'angle'}), angleSlider, angleReadout]), 18 | ]); 19 | const root = h('div', {id: 'root'}); 20 | document.body.appendChild(root); 21 | document.body.appendChild(controls); 22 | 23 | css(fs.readFileSync(path.join(__dirname, 'index.css'), 'utf8')); 24 | 25 | resl({ 26 | manifest: {mist: {type: 'image', src: 'mist.jpg'}}, 27 | onDone: ({mist}) => { 28 | regl({ 29 | pixelRatio: 1, 30 | container: root, 31 | attributes: {antialias: false}, 32 | onDone: require('fail-nicely')(regl => start(regl, mist)), 33 | optionalExtensions: ['oes_texture_float'], 34 | extensions: ['oes_texture_half_float'] 35 | }) 36 | } 37 | }) 38 | 39 | function start (regl, mist) { 40 | const width = regl._gl.canvas.width; 41 | const height = regl._gl.canvas.height; 42 | const img = regl.texture({data: mist, flipY: true}); 43 | const type = (regl.hasExtension('oes_texture_float') && !mobile) ? 'float' : 'half float'; 44 | const fbos = [0, 1, 2].map(() => regl.framebuffer({colorType: type, width: width, height: height})); 45 | 46 | const apply = regl({ 47 | vert: ` 48 | precision mediump float; 49 | attribute vec2 xy; 50 | void main () { 51 | gl_Position = vec4(xy, 0, 1); 52 | } 53 | `, 54 | frag: glsl(` 55 | precision highp float; 56 | #pragma glslify: fft = require(../) 57 | uniform sampler2D src; 58 | uniform vec2 resolution; 59 | uniform float subtransformSize, normalization; 60 | uniform bool horizontal, forward; 61 | 62 | void main () { 63 | gl_FragColor = fft(src, resolution, subtransformSize, horizontal, forward, normalization); 64 | } 65 | `), 66 | uniforms: { 67 | resolution: regl.prop('resolution'), 68 | forward: regl.prop('forward'), 69 | subtransformSize: regl.prop('subtransformSize'), 70 | horizontal: regl.prop('horizontal'), 71 | normalization: regl.prop('normalization'), 72 | src: regl.prop('input'), 73 | }, 74 | attributes: {xy: [-4, -4, 4, -4, 0, 4]}, 75 | framebuffer: regl.prop('output'), 76 | depth: {enable: false}, 77 | count: 3 78 | }); 79 | 80 | const filter = regl({ 81 | vert: ` 82 | precision mediump float; 83 | varying vec2 uv; 84 | attribute vec2 xy; 85 | void main () { 86 | uv = 0.5 * xy + 0.5; 87 | gl_Position = vec4(xy, 0, 1); 88 | } 89 | `, 90 | frag: glsl(` 91 | precision highp float; 92 | #pragma glslify: wavenumber = require(../wavenumber) 93 | varying vec2 uv; 94 | uniform sampler2D src; 95 | uniform vec2 resolution, e1; 96 | uniform float radius; 97 | 98 | void main () { 99 | vec4 col = texture2D(src, uv); 100 | vec2 kxy = wavenumber(resolution); 101 | float k1 = dot(e1, kxy) * radius; 102 | gl_FragColor = col * exp(-k1 * k1 * 0.5); 103 | } 104 | `), 105 | uniforms: { 106 | resolution: regl.prop('resolution'), 107 | radius: regl.prop('radius'), 108 | src: regl.prop('input'), 109 | e1: regl.prop('e1'), 110 | }, 111 | attributes: {xy: [-4, -4, 4, -4, 0, 4]}, 112 | framebuffer: regl.prop('output'), 113 | depth: {enable: false}, 114 | count: 3 115 | }); 116 | 117 | const forward = fft({ 118 | width: width, 119 | height: height, 120 | input: img, 121 | ping: fbos[0], 122 | pong: fbos[1], 123 | output: fbos[0], 124 | forward: true 125 | }); 126 | 127 | const inverse = fft({ 128 | width: width, 129 | height: height, 130 | input: fbos[1], 131 | ping: fbos[1], 132 | pong: fbos[2], 133 | forward: false 134 | }); 135 | 136 | apply(forward); 137 | 138 | function draw () { 139 | radiusReadout.textContent = radiusSlider.value; 140 | angleReadout.textContent = angleSlider.value; 141 | var theta = parseFloat(angleSlider.value) * Math.PI / 180.0; 142 | 143 | filter({ 144 | input: fbos[0], 145 | output: fbos[1], 146 | resolution: [1 / width, 1 / height], 147 | radius: parseFloat(radiusSlider.value), 148 | e1: [Math.cos(theta), Math.sin(theta)] 149 | }); 150 | 151 | apply(inverse); 152 | } 153 | 154 | radiusSlider.addEventListener('input', draw); 155 | angleSlider.addEventListener('input', draw); 156 | draw(); 157 | } 158 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # glsl-fft 2 | 3 | > GLSL setup for a [Fast Fourier Transform][fft] of two complex matrices 4 | 5 | ## Installation 6 | 7 | ```sh 8 | $ npm install glsl-fft 9 | ``` 10 | 11 | ## Example 12 | 13 | ```javascript 14 | var fft = require('glsl-fft'); 15 | 16 | // Set up a forward transform: 17 | var forwardTransform = fft({ 18 | width: 4, 19 | height: 2, 20 | input: 'a', 21 | ping: 'b', 22 | pong: 'c', 23 | output: 'd', 24 | forward: true, 25 | }); 26 | 27 | // Output is a list of passes: 28 | // => [ 29 | // {input: 'a', output: 'c', horizontal: true, forward: true, resolution: [ 0.25, 0.5 ], normalization: 1, subtransformSize: 2}, 30 | // {input: 'c', output: 'b', horizontal: true, forward: true, resolution: [ 0.25, 0.5 ], normalization: 1, subtransformSize: 4}, 31 | // {input: 'b', output: 'd', horizontal: false, forward: true, resolution: [ 0.25, 0.5 ], normalization: 1, subtransformSize: 2} 32 | // ] 33 | ``` 34 | 35 | Usage of the GLSL fragment shader using the above parameters as uniforms: 36 | 37 | ```glsl 38 | precision highp float; 39 | 40 | #pragma glslify: fft = require(glsl-fft) 41 | 42 | uniform sampler2D src; 43 | uniform vec2 resolution; 44 | uniform float subtransformSize, normalization; 45 | uniform bool horizontal, forward; 46 | 47 | void main () { 48 | gl_FragColor = fft(src, resolution, subtransformSize, horizontal, forward, normalization); 49 | } 50 | ``` 51 | 52 | See [example/index.js](./example/index.js) for a fully worked angular [Gaussian blur][gaussian] example using [regl][regl]. 53 | 54 | ## Usage 55 | 56 | ### What does it compute? 57 | 58 | This shader computes the 2D [Fast Fourier Transform][fft] of two complex input matrices contained in a single four-channel floating point (or half float) WebGL texture. The red and green channels contain the real and imaginary components of the first matrix, while the blue and alpha channels contain the real and imaginary components of the second matrix. The results match and are tested against [ndarray-fft][ndarray-fft]. 59 | 60 | ### What is required? 61 | 62 | This module does not interface with WebGL or have WebGL-specific peer dependencies. It only performs the setup work and exposes a fragment shader that performs the Fourier transform. 63 | 64 | This module is designed for use with [glslify][glslify], though it's not required. It also works relatively effortlessly with [regl][regl], though that's also not required. At minimum, you'll need no less than two float or half-float WebGL framebuffers, including input, output, and two buffers to ping-pong back and forth between during the passes. The ping-pong framebuffers may include the input and output framebuffers as long as the parity of the number of steps permits the final output without requiring an extra copy operation. 65 | 66 | The size of the textures must be a power of two, but not necessarily square. 67 | 68 | ### Is it fast? 69 | 70 | As far as fast Fourier transforms go, it's not really optimized at all, though it's faster than transferring data to and from the GPU each time you need to compute a Fourier transform. The biggest strike against it is the number of passes. That could be optimized, but I don't currently have the time. Would gladly accept a PR though. 71 | 72 | ## JavaScript API 73 | 74 | ### `require('glsl-fft')(options)` 75 | 76 | Perform the setup work required to use the FFT kernel in the fragment shader, `index.glsl`. Input arguments are: 77 | 78 | - `input` (`Any`): An identifier or object for the input framebuffer. 79 | - `output` (`Any`): An identifier or object for the final output framebuffer. 80 | - `ping` (`Any`): An identifier or object for the first ping-pong framebuffer. 81 | - `pong` (`Any`): An identifier or object for the second ping-pong framebuffer. 82 | - `forward` (`Boolean`): `true` if the transform is in the forward direction. 83 | - `size` (`Number`): size of the input, equal to the `width` and `height`. Must be a power of two. 84 | - `width` (`Number`): width of the input. Must be a power of two. Ignored if `size` is specified. 85 | - `height` (`Number`): height of the input. Must be a power of two. Ignored if `size` is specifid. 86 | - `splitNormalization`: (`Boolean`): If `true`, normalize by `1 / √(width * height)` on both the forward and inverse transforms. If `false`, normalize by `1 / (width * height)` on only the inverse transform. Default is `true`. Provided to avoid catastrophic overflow during the forward transform when using half-float textures. One-way transforms will match [ndarray-fft][ndarray-fft] only if `false`. 87 | 88 | Returns a list of passes. Each object in the list is a set of parameters that must either be used to bind the correct framebuffers or passed as uniforms to the fragment shader. 89 | 90 | ## GLSL API 91 | 92 | ### `#pragma glslify: fft = require(glsl-fft)` 93 | ### `vec4 fft(sampler2D src, vec2 resolution, float subtransformSize, bool horizontal, bool forward, float normalization)` 94 | 95 | Returns the `gl_FragColor` in order to perform a single pass of the FFT comptuation. Uniforms map directly to the output of the JavaScript setup function, with the exception of `src` which is a `sampler2D` for the input framebuffer or texture. 96 | 97 | ### `#pragma glslify: wavenumber = require(glsl-fft/wavenumber)` 98 | ### `vec2 wavenumber(vec2 resolution)` 99 | ### `vec2 wavenumber(vec2 resolution, float dxy)` 100 | ### `vec2 wavenumber(vec2 resolution, vec2 dxy)` 101 | 102 | Parameters are: 103 | - `resolution`: a `vec2` containing `1 / width` and `1 / height`. 104 | - `dxy` (optional): Either a float representing the sample spacing in either direction, or a `vec2` representing the sample spacing in the horizontal and vertical directions, respectively. 105 | 106 | Returns `vec2(kx, ky)`, where `kx` and `ky` are the angular wavenumbers of the corresponding texel of the Fourier Transformed data. 107 | 108 | ## See also 109 | 110 | - [ndarray-fft][ndarray-fft] 111 | - [glsl-rfft][glsl-rfft] 112 | 113 | ## License 114 | 115 | © Ricky Reusser 2017. MIT License. Based on the [filtering example][dli] of David Li. See LICENSE for more details. 116 | 117 | [glslify]: https://github.com/glslify/glslify 118 | [fft]: https://en.wikipedia.org/wiki/Fast_Fourier_transform 119 | [dli]: https://github.com/dli/filtering 120 | [regl]: https://github.com/regl-project/regl 121 | [ndarray-fft]: https://github.com/scijs/ndarray-fft 122 | [gaussian]: https://en.wikipedia.org/wiki/Gaussian_blur 123 | [glsl-rfft]: https://github.com/rreusser/glsl-rfft 124 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var regl = require('regl'); 3 | var transform = require('../'); 4 | var iota = require('iota-array'); 5 | var glsl = require('glslify'); 6 | var show = require('ndarray-show'); 7 | var ndarray = require('ndarray'); 8 | var ndFft = require('ndarray-fft'); 9 | var pool = require('ndarray-scratch'); 10 | var almostEqual = require('almost-equal'); 11 | 12 | var seed = 1; 13 | function random () { 14 | seed = (seed * 9301 + 49297) % 233280; 15 | return seed / 233280; 16 | } 17 | 18 | test('throws if size is not a power of two', function (t) { 19 | t.throws(function () { 20 | transform({ 21 | size: 9, 22 | input: 'a', 23 | ping: 'b', 24 | pong: 'c', 25 | output: 'd', 26 | forward: true, 27 | splitNormalization: false 28 | }); 29 | }, /must be a power of two/); 30 | 31 | t.end(); 32 | }); 33 | 34 | test('throws neither size nor height+width are provided', function (t) { 35 | t.throws(function () { 36 | transform({ 37 | input: 'a', 38 | ping: 'b', 39 | pong: 'c', 40 | output: 'd', 41 | forward: true, 42 | splitNormalization: false 43 | }); 44 | }, /either size or both width and height/); 45 | 46 | t.end(); 47 | }); 48 | 49 | test('forward fft', function (t) { 50 | var fft = transform({ 51 | size: 32, 52 | input: 'a', 53 | ping: 'b', 54 | pong: 'c', 55 | output: 'd', 56 | forward: true, 57 | splitNormalization: false 58 | }); 59 | 60 | t.deepEqual(fft, [ 61 | { forward: true, input: 'a', output: 'c', normalization: 1, horizontal: true, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 62 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 63 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: true, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 64 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 65 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: true, subtransformSize: 32, resolution: [0.03125, 0.03125]}, 66 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: false, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 67 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 68 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: false, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 69 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 70 | { forward: true, input: 'c', output: 'd', normalization: 1, horizontal: false, subtransformSize: 32, resolution: [0.03125, 0.03125]} 71 | ]); 72 | 73 | t.end(); 74 | }); 75 | 76 | test('forward fft avoids input framebuffer collisions', function (t) { 77 | var fft = transform({ 78 | size: 32, 79 | input: 'a', 80 | ping: 'b', 81 | pong: 'a', 82 | output: 'c', 83 | forward: true, 84 | splitNormalization: false 85 | }); 86 | 87 | t.deepEqual(fft, [ 88 | { forward: true, input: 'a', output: 'b', normalization: 1, horizontal: true, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 89 | { forward: true, input: 'b', output: 'a', normalization: 1, horizontal: true, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 90 | { forward: true, input: 'a', output: 'b', normalization: 1, horizontal: true, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 91 | { forward: true, input: 'b', output: 'a', normalization: 1, horizontal: true, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 92 | { forward: true, input: 'a', output: 'b', normalization: 1, horizontal: true, subtransformSize: 32, resolution: [0.03125, 0.03125]}, 93 | { forward: true, input: 'b', output: 'a', normalization: 1, horizontal: false, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 94 | { forward: true, input: 'a', output: 'b', normalization: 1, horizontal: false, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 95 | { forward: true, input: 'b', output: 'a', normalization: 1, horizontal: false, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 96 | { forward: true, input: 'a', output: 'b', normalization: 1, horizontal: false, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 97 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 32, resolution: [0.03125, 0.03125]} 98 | ]); 99 | 100 | t.end(); 101 | }); 102 | 103 | test('forward fft avoids output framebuffer collisions', function (t) { 104 | var fft = transform({ 105 | size: 32, 106 | input: 'a', 107 | ping: 'b', 108 | pong: 'c', 109 | output: 'c', 110 | forward: true, 111 | splitNormalization: false 112 | }); 113 | 114 | t.deepEqual(fft, [ 115 | { forward: true, input: 'a', output: 'b', normalization: 1, horizontal: true, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 116 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: true, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 117 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 118 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: true, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 119 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 32, resolution: [0.03125, 0.03125]}, 120 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 121 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: false, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 122 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 123 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: false, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 124 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 32, resolution: [0.03125, 0.03125]} 125 | ]); 126 | 127 | t.end(); 128 | }); 129 | 130 | test('detects input+output framebuffer collisions', function (t) { 131 | t.throws(function () { 132 | transform({ 133 | size: 32, 134 | input: 'a', 135 | ping: 'b', 136 | pong: 'a', 137 | output: 'b', 138 | forward: true, 139 | splitNormalization: false 140 | }); 141 | }, /not enough framebuffers to compute/); 142 | 143 | t.end(); 144 | }); 145 | 146 | test('non-square forward fft', function (t) { 147 | var fft = transform({ 148 | width: 16, 149 | height: 8, 150 | input: 'a', 151 | ping: 'b', 152 | pong: 'c', 153 | output: 'd', 154 | forward: true, 155 | splitNormalization: false 156 | }); 157 | 158 | t.deepEqual(fft, [ 159 | { forward: true, input: 'a', output: 'c', normalization: 1, horizontal: true, subtransformSize: 2, resolution: [0.0625, 0.125]}, 160 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 4, resolution: [0.0625, 0.125]}, 161 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: true, subtransformSize: 8, resolution: [0.0625, 0.125]}, 162 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 16, resolution: [0.0625, 0.125]}, 163 | { forward: true, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 2, resolution: [0.0625, 0.125]}, 164 | { forward: true, input: 'c', output: 'b', normalization: 1, horizontal: false, subtransformSize: 4, resolution: [0.0625, 0.125]}, 165 | { forward: true, input: 'b', output: 'd', normalization: 1, horizontal: false, subtransformSize: 8, resolution: [0.0625, 0.125]} 166 | ]); 167 | 168 | t.end(); 169 | }); 170 | 171 | test('inverse fft', function (t) { 172 | var fft = transform({ 173 | size: 32, 174 | input: 'a', 175 | ping: 'b', 176 | pong: 'c', 177 | output: 'd', 178 | forward: false, 179 | splitNormalization: false 180 | }); 181 | 182 | t.deepEqual(fft, [ 183 | { forward: false, input: 'a', output: 'c', normalization: 0.0009765625, horizontal: true, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 184 | { forward: false, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 185 | { forward: false, input: 'b', output: 'c', normalization: 1, horizontal: true, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 186 | { forward: false, input: 'c', output: 'b', normalization: 1, horizontal: true, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 187 | { forward: false, input: 'b', output: 'c', normalization: 1, horizontal: true, subtransformSize: 32, resolution: [0.03125, 0.03125]}, 188 | { forward: false, input: 'c', output: 'b', normalization: 1, horizontal: false, subtransformSize: 2, resolution: [0.03125, 0.03125]}, 189 | { forward: false, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 4, resolution: [0.03125, 0.03125]}, 190 | { forward: false, input: 'c', output: 'b', normalization: 1, horizontal: false, subtransformSize: 8, resolution: [0.03125, 0.03125]}, 191 | { forward: false, input: 'b', output: 'c', normalization: 1, horizontal: false, subtransformSize: 16, resolution: [0.03125, 0.03125]}, 192 | { forward: false, input: 'c', output: 'd', normalization: 1, horizontal: false, subtransformSize: 32, resolution: [0.03125, 0.03125]} 193 | ]); 194 | 195 | t.end(); 196 | }); 197 | 198 | test('regl', function (t) { 199 | var canvas = document.createElement('canvas'); 200 | canvas.width = 8; 201 | canvas.height = 8; 202 | 203 | regl({ 204 | canvas: canvas, 205 | extensions: ['oes_texture_float'], 206 | onDone: function (err, regl) { 207 | if (err) { 208 | t.notOk('fail'); 209 | t.end(); 210 | } 211 | 212 | var applyFFT = regl({ 213 | vert: ` 214 | precision mediump float; 215 | attribute vec2 xy; 216 | void main () { 217 | gl_Position = vec4(xy, 0, 1); 218 | } 219 | `, 220 | frag: glsl(` 221 | precision highp float; 222 | #pragma glslify: fft = require(../) 223 | uniform sampler2D src; 224 | uniform vec2 resolution; 225 | uniform float subtransformSize, normalization; 226 | uniform bool horizontal, forward; 227 | 228 | void main () { 229 | gl_FragColor = fft( 230 | src, 231 | resolution, 232 | subtransformSize, 233 | horizontal, 234 | forward, 235 | normalization 236 | ); 237 | } 238 | `), 239 | uniforms: { 240 | resolution: regl.prop('resolution'), 241 | forward: regl.prop('forward'), 242 | subtransformSize: regl.prop('subtransformSize'), 243 | horizontal: regl.prop('horizontal'), 244 | normalization: regl.prop('normalization'), 245 | src: regl.prop('input'), 246 | }, 247 | attributes: {xy: [-4, -4, 4, -4, 0, 4]}, 248 | framebuffer: regl.prop('output'), 249 | depth: {enable: false}, 250 | count: 3 251 | }); 252 | 253 | var drawWavenumber = regl({ 254 | vert: ` 255 | precision mediump float; 256 | attribute vec2 xy; 257 | varying vec2 uv; 258 | void main () { 259 | uv = 0.5 * xy + 0.5; 260 | gl_Position = vec4(xy, 0, 1); 261 | } 262 | `, 263 | frag: glsl(` 264 | precision highp float; 265 | #pragma glslify: wavenumber = require(../wavenumber) 266 | varying vec2 uv; 267 | uniform vec2 resolution; 268 | 269 | void main () { 270 | gl_FragColor = vec4(wavenumber(resolution), 0, 0); 271 | } 272 | `), 273 | uniforms: {resolution: regl.prop('resolution')}, 274 | attributes: {xy: [-4, -4, 4, -4, 0, 4]}, 275 | framebuffer: regl.prop('output'), 276 | depth: {enable: false}, 277 | count: 3 278 | }); 279 | 280 | function compare (actual, expected, tol) { 281 | for (var i = 0; i < expected.length; i++) { 282 | if (!almostEqual(expected[i], actual[i], tol, tol)) { 283 | t.notOk(i + ': ' + actual[i] + ' (actual) !~ ' + expected[i] + ' (expected) (tol=' + tol + ')'); 284 | return false; 285 | } 286 | } 287 | return true; 288 | } 289 | 290 | function fftfreq (i, n, dx) { 291 | return ((i < Math.floor((n + 1) / 2)) ? i / (n * dx) : -(n - i) / (n * dx)) * 2 * Math.PI; 292 | } 293 | 294 | function testWavenumber (width, height) { 295 | var expected = new Array(width * height * 4).fill(0); 296 | for (var i = 0; i < width; i++) { 297 | for (var j = 0; j < height; j++) { 298 | var idx = 4 * (i * height + j); 299 | expected[idx] = fftfreq(i, width, 1.0); 300 | expected[idx + 1] = fftfreq(j, height, 1.0); 301 | expected[idx + 2] = 0.0; 302 | expected[idx + 3] = 0.0; 303 | } 304 | } 305 | 306 | var ndExpected = ndarray(expected, [width, height, 4]); 307 | //console.log('kx = \n' + show(ndExpected.pick(null, null, 0)) + '\n'); 308 | //console.log('ky = \n' + show(ndExpected.pick(null, null, 1)) + '\n'); 309 | 310 | var fbo = regl.framebuffer({ 311 | colorType: 'float', 312 | colorFormat: 'rgba', 313 | width: width, 314 | height: height 315 | }); 316 | 317 | drawWavenumber({ 318 | resolution: [1 / width, 1 / height], 319 | output: fbo 320 | }); 321 | 322 | var ndOut; 323 | fbo.use(function () { 324 | var data = regl.read(); 325 | ndOut = ndarray(data, [height, width, 4]).transpose(1, 0); 326 | 327 | //console.log('kx:\n' + show(ndOut.pick(null, null, 0)) + '\n'); 328 | //console.log('ky:\n' + show(ndOut.pick(null, null, 1)) + '\n'); 329 | 330 | var clone = pool.clone(ndOut) 331 | t.ok(compare(clone.data, ndExpected.data, 1e-4), 'wavenumber: ' + width + ' x ' + height); 332 | pool.free(clone); 333 | }); 334 | 335 | fbo.destroy(); 336 | } 337 | 338 | function testFFT (direction, width, height, tol) { 339 | var input = new Array(width * height * 4).fill(0).map(random); 340 | 341 | var A = ndarray(new Float32Array(input), [width, height, 2, 2]); 342 | 343 | // console.log('Ar = \n' + show(A.pick(null, null, 0, 0))); 344 | // console.log('Ar = \n' + show(A.pick(null, null, 0, 1))); 345 | 346 | ndFft(direction, A.pick(null, null, 0, 0), A.pick(null, null, 0, 1)); 347 | ndFft(direction, A.pick(null, null, 1, 0), A.pick(null, null, 1, 1)); 348 | 349 | var fbos = [0, 1, 2].map(function () { 350 | return regl.framebuffer({ 351 | color: regl.texture({ 352 | type: 'float', 353 | format: 'rgba', 354 | data: ndarray(input, [width, height, 4]), 355 | width: width, 356 | height: height 357 | }) 358 | }) 359 | }); 360 | 361 | var fft = transform({ 362 | splitNormalization: false, 363 | width: width, 364 | height: height, 365 | input: fbos[0], 366 | ping: fbos[0], 367 | pong: fbos[1], 368 | output: fbos[2], 369 | forward: direction > 0 ? true : false 370 | }); 371 | 372 | applyFFT(fft); 373 | 374 | var ndOut; 375 | fbos[2].use(function () { 376 | var data = regl.read(); 377 | ndOut = ndarray(data, [height, width, 2, 2]).transpose(1, 0); 378 | 379 | // Flatten this ndarray so we can compare internal data: 380 | var clone = pool.clone(ndOut) 381 | t.ok(compare(clone.data, A.data, tol), (direction > 0 ? 'forward ' : 'inverse ') + width + ' x ' + height); 382 | pool.free(clone); 383 | }); 384 | 385 | // console.log('Ahr (expected) = \n' + show(A.pick(null, null, 0, 0)) + '\n'); 386 | // console.log('Ahr = \n' + show(ndOut.pick(null, null, 0, 0)) + '\n\n'); 387 | // console.log('Ahi (expected) = \n' + show(A.pick(null, null, 0, 1)) + '\n'); 388 | // console.log('Ahi = \n' + show(ndOut.pick(null, null, 0, 1)) + '\n'); 389 | 390 | fbos.forEach(function (fbo) { 391 | fbo.destroy(); 392 | }); 393 | } 394 | 395 | testFFT(1, 2, 2, 1e-5); 396 | testFFT(1, 4, 2, 1e-5); 397 | testFFT(1, 8, 2, 1e-4); 398 | testFFT(1, 16, 2, 1e-4); 399 | testFFT(1, 32, 2, 1e-3); 400 | 401 | testFFT(1, 1, 8, 1e-5); 402 | testFFT(1, 2, 8, 1e-5); 403 | testFFT(1, 4, 8, 1e-4); 404 | testFFT(1, 8, 8, 1e-4); 405 | testFFT(1, 16, 8, 1e-4); 406 | testFFT(1, 32, 8, 1e-3); 407 | 408 | testFFT(-1, 1, 8, 1e-5); 409 | testFFT(-1, 2, 8, 1e-5); 410 | testFFT(-1, 4, 8, 1e-5); 411 | testFFT(-1, 8, 8, 1e-4); 412 | testFFT(-1, 16, 8, 1e-4); 413 | testFFT(-1, 32, 8, 1e-3); 414 | 415 | testFFT(1, 1, 8, 1e-5); 416 | testFFT(1, 2, 8, 1e-5); 417 | testFFT(1, 4, 8, 1e-5); 418 | testFFT(1, 8, 8, 1e-4); 419 | testFFT(1, 16, 8, 1e-4); 420 | testFFT(1, 32, 8, 1e-3); 421 | 422 | testFFT(-1, 1, 8, 1e-5); 423 | testFFT(-1, 2, 8, 1e-5); 424 | testFFT(-1, 4, 8, 1e-5); 425 | testFFT(-1, 8, 8, 1e-4); 426 | testFFT(-1, 16, 8, 1e-4); 427 | testFFT(-1, 32, 8, 1e-3); 428 | 429 | testFFT(1, 32, 64, 1e-2); 430 | testFFT(-1, 32, 64, 1e-4); 431 | testFFT(1, 64, 32, 1e-2); 432 | testFFT(-1, 64, 32, 1e-5); 433 | testFFT(1, 64, 64, 1e-2); 434 | testFFT(-1, 64, 64, 1e-5); 435 | 436 | 437 | for (i = 0; i < 6; i++) { 438 | for (j = 0; j < 6; j++) { 439 | testWavenumber(Math.pow(2, i), Math.pow(2, j)); 440 | } 441 | } 442 | 443 | testWavenumber(128, 128); 444 | 445 | regl.destroy(); 446 | 447 | t.end(); 448 | } 449 | }); 450 | }); 451 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |