Last active
June 6, 2021 11:56
-
-
Save haxiomic/d4a61defd3790553b20ae2ad2ceef9a5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as THREE from 'three'; | |
function glslFloat(f) { | |
let s = f + ''; | |
if (s.indexOf('.') == -1) { | |
s += '.'; | |
} | |
return s; | |
} | |
/** | |
* Fast fixed-kernel gaussian blur | |
* @author haxiomic (George Corney) | |
*/ | |
export default class Blur1DMaterial extends THREE.RawShaderMaterial { | |
uTexture; | |
uTexelSize; | |
kernel; | |
directionX; | |
directionY; | |
constructor(kernel, truncationSigma, directionX, directionY, linearSampling) { | |
super({ | |
uniforms: { | |
texture: null, | |
invResolution: null, | |
}, | |
}); | |
this.uniforms.texture = this.uTexture = new THREE.Uniform(null); | |
this.uniforms.invResolution = this.uTexelSize = new THREE.Uniform(new THREE.Vector2(1,1)); | |
this.kernel = kernel; | |
this.directionX = directionX; | |
this.directionY = directionY; | |
this.side = THREE.DoubleSide; | |
let shaderParts = this.generateShaderParts(kernel, truncationSigma, directionX, directionY, linearSampling); | |
let precision = 'mediump'; | |
this.vertexShader = ` | |
precision ${precision} float; | |
attribute vec2 position; | |
uniform vec2 invResolution; | |
\n${shaderParts.varyingDeclarations.join('\n')} | |
const vec2 madd = vec2(0.5, 0.5); | |
void main() { | |
vec2 texelCoord = (position * madd + madd); | |
\n${shaderParts.varyingValues.join('\n')} | |
gl_Position = vec4(position, 0.0, 1.); | |
} | |
`; | |
this.fragmentShader = ` | |
precision ${precision} float; | |
uniform sampler2D texture; | |
\n${shaderParts.fragmentDeclarations.join('\n')} | |
\n${shaderParts.varyingDeclarations.join('\n')} | |
void main() { | |
\n${shaderParts.fragmentVariables.join('\n')} | |
vec4 blend = vec4(0.0); | |
\n${shaderParts.textureSamples.join('\n')}; | |
gl_FragColor = blend; | |
} | |
`; | |
} | |
generateShaderParts(kernel, truncationSigma, directionX, directionY, linearSampling) { | |
// Generate sampling offsets and weights | |
let N = Blur1DMaterial.nearestBestKernel(kernel); | |
let centerIndex = (N - 1) / 2; | |
// Generate Gaussian sampling weights over kernel | |
let offsets = []; | |
let weights = []; | |
let totalWeight = 0.0; | |
for (let i = 0; i < N; i++) { | |
let u = i / (N - 1); | |
let w = Blur1DMaterial.gaussianWeight(u * 2.0 - 1, truncationSigma); | |
offsets[i] = (i - centerIndex); | |
weights[i] = w; | |
totalWeight += w; | |
} | |
// Normalize weights | |
for (let i = 0; i < weights.length; i++) { | |
weights[i] /= totalWeight; | |
} | |
/** | |
Optimize: combine samples to take advantage of hardware linear sampling | |
Let weights of two samples be A and B | |
Then the sum of the samples: `Ax + By` | |
Can be represented with a single lerp sample a (distance to sample x), with new weight W | |
`Ax + By = W((1-a)x + ay)` | |
Solving for W, a in terms of A, B: | |
`W = A + B` | |
`a = B/(A + B)` | |
**/ | |
let optimizeSamples = linearSampling; | |
if (optimizeSamples) { | |
let lerpSampleOffsets = []; | |
let lerpSampleWeights = []; | |
let i = 0; | |
while(i < N) { | |
let A = weights[i]; | |
let leftOffset = offsets[i]; | |
if ((i + 1) < N) { | |
// there is a pair to combine with | |
let B = weights[i + 1]; | |
let lerpWeight = A + B; | |
let alpha = B/(A + B); | |
let lerpOffset = leftOffset + alpha; | |
lerpSampleOffsets.push(lerpOffset); | |
lerpSampleWeights.push(lerpWeight); | |
} else { | |
lerpSampleOffsets.push(leftOffset); | |
lerpSampleWeights.push(A); | |
} | |
i += 2; | |
} | |
// replace with optimized | |
offsets = lerpSampleOffsets; | |
weights = lerpSampleWeights; | |
} | |
// Generate shaders | |
let maxVaryingRows = 512; // ! this should be determined by querying the gl context | |
let maxVaryingVec2 = maxVaryingRows * 2; // seems like 2 varyings per row, but is this the guaranteed packing? | |
let varyingCount = Math.floor(Math.min(offsets.length, maxVaryingVec2)); | |
let varyingDeclarations = []; | |
for (let i = 0; i < varyingCount; i++) { | |
varyingDeclarations.push(`varying vec2 sampleCoord${i};`); | |
} | |
let varyingValues = []; | |
for (let i = 0; i < varyingCount; i++) { | |
varyingValues.push(`sampleCoord${i} = texelCoord + vec2(${glslFloat(offsets[i] * directionX)}, ${glslFloat(offsets[i] * directionY)}) * invResolution;`); | |
} | |
let fragmentVariables = []; | |
for (let i = varyingCount; i < offsets.length; i++) { | |
fragmentVariables.push(`vec2 sampleCoord${i} = sampleCoord0 + vec2(${glslFloat((offsets[i] - offsets[0]) * directionX)}, ${glslFloat((offsets[i] - offsets[0]) * directionY)}) * invResolution;`); | |
} | |
let textureSamples = []; | |
for (let i = 0; i < offsets.length; i++) { | |
textureSamples.push(`blend += texture2D(texture, sampleCoord${i}) * ${glslFloat(weights[i])};`); | |
} | |
return { | |
varyingDeclarations: varyingDeclarations, | |
varyingValues: varyingValues, | |
fragmentDeclarations: varyingCount < offsets.length ? ['uniform vec2 invResolution;'] : [''], | |
fragmentVariables: fragmentVariables, | |
textureSamples: textureSamples, | |
}; | |
} | |
static nearestBestKernel(idealKernel) { | |
let v = Math.round(idealKernel); | |
let ks = [v, v - 1, v + 1, v - 2, v + 2]; | |
// for (k in [v, v - 1, v + 1, v - 2, v + 2]) { | |
for (let k of ks) { | |
if (((k % 2) != 0) && ((Math.floor(k / 2) % 2) == 0) && k > 0) { | |
return Math.floor(Math.max(k, 3)); | |
} | |
} | |
return Math.floor(Math.max(v, 3)); | |
} | |
static gaussianWeight(x, truncationSigma) { | |
let sigma = truncationSigma; | |
let denominator = Math.sqrt(2.0 * Math.PI) * sigma; | |
let exponent = -((x * x) / (2.0 * sigma * sigma)); | |
let weight = (1.0 / denominator) * Math.exp(exponent); | |
return weight; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Fast fixed gaussian blur for three.js
Uses texture coordinates computed in vertex shader to enable early texture fetch and automatically determines optimal sampling to take advantage of hardware lerp (ensure sampled textures use linear filtering)
First written in TS for BabylonJS's PBR pipeline back in ~ 2016
For truncationSigma try 0.5