diff --git a/js/bloomPass.js b/js/bloomPass.js index bd91531..2ece3b2 100644 --- a/js/bloomPass.js +++ b/js/bloomPass.js @@ -1,4 +1,5 @@ import { + loadText, extractEntries, makePassFBO, makePyramid, @@ -39,21 +40,11 @@ export default (regl, config, inputs) => { const vBlurPyramid = makePyramid(regl, pyramidHeight, config.useHalfFloat); const output = makePassFBO(regl, config.useHalfFloat); + const highPassFrag = loadText("../shaders/highPass.frag"); + // The high pass restricts the blur to bright things in our input texture. const highPass = regl({ - frag: ` - precision mediump float; - varying vec2 vUV; - uniform sampler2D tex; - uniform float highPassThreshold; - void main() { - vec3 lumaColor = texture2D(tex, vUV).rgb; - if (lumaColor.r < highPassThreshold) lumaColor.r = 0.0; - if (lumaColor.g < highPassThreshold) lumaColor.g = 0.0; - if (lumaColor.b < highPassThreshold) lumaColor.b = 0.0; - gl_FragColor = vec4(lumaColor, 1.0); - } - `, + frag: regl.prop("frag"), uniforms: { ...uniforms, tex: regl.prop("tex") @@ -64,33 +55,10 @@ export default (regl, config, inputs) => { // A 2D gaussian blur is just a 1D blur done horizontally, then done vertically. // The FBO pyramid's levels represent separate levels of detail; // by blurring them all, this 3x1 blur approximates a more complex gaussian. + + const blurFrag = loadText("../shaders/blur.frag"); const blur = regl({ - frag: ` - precision mediump float; - uniform float width, height; - uniform sampler2D tex; - uniform vec2 direction; - varying vec2 vUV; - void main() { - vec2 size = width > height ? vec2(width / height, 1.) : vec2(1., height / width); - gl_FragColor = - texture2D(tex, vUV) * 0.442 + - ( - texture2D(tex, vUV + direction / max(width, height) * size) + - texture2D(tex, vUV - direction / max(width, height) * size) - ) * 0.279; - // gl_FragColor = - // texture2D(tex, vUV) * 0.38774 + - // ( - // texture2D(tex, vUV + direction / max(width, height) * size * 0.5) + - // texture2D(tex, vUV - direction / max(width, height) * size * 0.5) - // ) * 0.24477 + - // ( - // texture2D(tex, vUV + direction / max(width, height) * size) + - // texture2D(tex, vUV - direction / max(width, height) * size) - // ) * 0.06136; - } - `, + frag: regl.prop("frag"), uniforms: { ...uniforms, tex: regl.prop("tex"), @@ -140,9 +108,9 @@ export default (regl, config, inputs) => { const highPassFBO = highPassPyramid[i]; const hBlurFBO = hBlurPyramid[i]; const vBlurFBO = vBlurPyramid[i]; - highPass({ fbo: highPassFBO, tex: inputs.primary }); - blur({ fbo: hBlurFBO, tex: highPassFBO, direction: [1, 0] }); - blur({ fbo: vBlurFBO, tex: hBlurFBO, direction: [0, 1] }); + highPass({ fbo: highPassFBO, frag: highPassFrag.text(), tex: inputs.primary }); + blur({ fbo: hBlurFBO, frag: blurFrag.text(), tex: highPassFBO, direction: [1, 0] }); + blur({ fbo: vBlurFBO, frag: blurFrag.text(), tex: hBlurFBO, direction: [0, 1] }); } flattenPyramid(); @@ -153,6 +121,7 @@ export default (regl, config, inputs) => { resizePyramid(hBlurPyramid, w, h, config.bloomSize); resizePyramid(vBlurPyramid, w, h, config.bloomSize); output.resize(w, h); - } + }, + [highPassFrag.laoded, blurFrag.loaded] ); }; diff --git a/js/imagePass.js b/js/imagePass.js index bb7e6fd..749c64a 100644 --- a/js/imagePass.js +++ b/js/imagePass.js @@ -1,4 +1,4 @@ -import { loadImage, makePassFBO, makePass } from "./utils.js"; +import { loadImage, loadText, makePassFBO, makePass } from "./utils.js"; const defaultBGURL = "https://upload.wikimedia.org/wikipedia/commons/0/0a/Flammarion_Colored.jpg"; @@ -7,32 +7,22 @@ export default (regl, config, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); const bgURL = "bgURL" in config ? config.bgURL : defaultBGURL; const background = loadImage(regl, bgURL); + const imagePassFrag = loadText("../shaders/imagePass.frag"); + const render = regl({ + frag: regl.prop("frag"), + uniforms: { + backgroundTex: background.texture, + tex: inputs.primary, + bloomTex: inputs.bloom + }, + framebuffer: output + }); return makePass( { primary: output }, - regl({ - frag: ` - precision mediump float; - uniform sampler2D tex; - uniform sampler2D bloomTex; - uniform sampler2D backgroundTex; - varying vec2 vUV; - - void main() { - vec3 bgColor = texture2D(backgroundTex, vUV).rgb; - float brightness = pow(min(1., texture2D(tex, vUV).r * 2.) + texture2D(bloomTex, vUV).r, 1.5); - gl_FragColor = vec4(bgColor * brightness, 1.0); - } - `, - uniforms: { - backgroundTex: background.texture, - tex: inputs.primary, - bloomTex: inputs.bloom - }, - framebuffer: output - }), + () => render({frag: imagePassFrag.text()}), null, - background.loaded + [background.loaded, imagePassFrag.loaded] ); }; diff --git a/js/main.js b/js/main.js index e3823e6..b19a4a3 100644 --- a/js/main.js +++ b/js/main.js @@ -75,13 +75,13 @@ document.body.onload = async () => { ) { dimensions.width = viewportWidth; dimensions.height = viewportHeight; - for (const pass of pipeline) { - pass.resize(viewportWidth, viewportHeight); + for (const step of pipeline) { + step.resize(viewportWidth, viewportHeight); } } fullScreenQuad(() => { - for (const pass of pipeline) { - pass.render(); + for (const step of pipeline) { + step.render(); } drawToScreen(); }); diff --git a/js/palettePass.js b/js/palettePass.js index d7ce755..8a330ba 100644 --- a/js/palettePass.js +++ b/js/palettePass.js @@ -1,4 +1,4 @@ -import { extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; +import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; const colorToRGB = ([hue, saturation, lightness]) => { const a = saturation * Math.min(lightness, 1 - lightness); @@ -58,47 +58,29 @@ export default (regl, config, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); const palette = makePalette(regl, config.paletteEntries); + const palettePassFrag = loadText("../shaders/palettePass.frag"); + + const render = regl({ + frag: regl.prop("frag"), + + uniforms: { + ...extractEntries(config, [ + "backgroundColor", + ]), + tex: inputs.primary, + bloomTex: inputs.bloom, + palette, + ditherMagnitude: 0.05 + }, + framebuffer: output + }); + return makePass( { primary: output }, - regl({ - frag: ` - precision mediump float; - #define PI 3.14159265359 - - uniform sampler2D tex; - uniform sampler2D bloomTex; - uniform sampler2D palette; - uniform float ditherMagnitude; - uniform float time; - uniform vec3 backgroundColor; - varying vec2 vUV; - - highp float rand( const in vec2 uv, const in float t ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract(sin(sn) * c + t); - } - - void main() { - vec4 brightnessRGB = texture2D( tex, vUV ) + texture2D( bloomTex, vUV ); - float brightness = brightnessRGB.r + brightnessRGB.g + brightnessRGB.b; - float at = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude; - gl_FragColor = texture2D( palette, vec2(at, 0.0)) + vec4(backgroundColor, 0.0); - } - `, - - uniforms: { - ...extractEntries(config, [ - "backgroundColor", - ]), - tex: inputs.primary, - bloomTex: inputs.bloom, - palette, - ditherMagnitude: 0.05 - }, - framebuffer: output - }) + () => render({ frag: palettePassFrag.text() }), + null, + palettePassFrag.loaded ); }; diff --git a/js/renderer.js b/js/renderer.js index b22f1ba..b6289bc 100644 --- a/js/renderer.js +++ b/js/renderer.js @@ -1,6 +1,7 @@ import { extractEntries, loadImage, + loadText, makePassFBO, makeDoubleBuffer, makePass @@ -96,187 +97,9 @@ export default (regl, config) => { const msdf = loadImage(regl, config.glyphTexURL); - // This shader is the star of the show. - // In normal operation, each pixel represents a glyph's: - // R: brightness - // G: progress through the glyph sequence - // B: current glyph index - // A: additional brightness, for effects + const updateFrag = loadText("../shaders/update.frag"); const update = regl({ - frag: ` - precision highp float; - - #define PI 3.14159265359 - #define RADS_TO_HZ 0.15915494309 - #define SQRT_2 1.4142135623730951 - #define SQRT_5 2.23606797749979 - - uniform float time; - uniform float numColumns, numRows; - uniform sampler2D lastState; - uniform bool hasSun; - uniform bool hasThunder; - uniform bool showComputationTexture; - uniform float brightnessMinimum, brightnessMultiplier, brightnessOffset, brightnessMix; - uniform float animationSpeed, fallSpeed, cycleSpeed; - uniform float raindropLength; - uniform float glyphHeightToWidth; - uniform int cycleStyle; - uniform float rippleScale, rippleSpeed, rippleThickness; - uniform int rippleType; - uniform float cursorEffectThreshold; - - float max2(vec2 v) { - return max(v.x, v.y); - } - - highp float rand( const in vec2 uv ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract(sin(sn) * c); - } - - vec2 rand2(vec2 p) { - return fract(vec2(sin(p.x * 591.32 + p.y * 154.077), cos(p.x * 391.32 + p.y * 49.077))); - } - - float getRainTime(float simTime, vec2 glyphPos) { - float columnTimeOffset = rand(vec2(glyphPos.x, 0.0)); - float columnSpeedOffset = rand(vec2(glyphPos.x + 0.1, 0.0)); - // columnSpeedOffset = 0.0; // loop - float columnTime = (columnTimeOffset * 1000.0 + simTime * 0.5 * fallSpeed) * (0.5 + columnSpeedOffset * 0.5) + (sin(RADS_TO_HZ * simTime * fallSpeed * columnSpeedOffset) * 0.2); - return (glyphPos.y * 0.01 + columnTime) / raindropLength; - } - - float getRainBrightness(float rainTime) { - float value = 1.0 - fract((rainTime + 0.3 * sin(RADS_TO_HZ * SQRT_2 * rainTime) + 0.2 * sin(RADS_TO_HZ * SQRT_5 * rainTime))); - // value = 1.0 - fract(rainTime); // loop - return log(value * 1.25) * 3.0; - } - - float getGlyphCycleSpeed(float rainTime, float brightness) { - float glyphCycleSpeed = 0.0; - if (cycleStyle == 0 && brightness > 0.0) { - glyphCycleSpeed = pow(1.0 - brightness, 4.0); - } else if (cycleStyle == 1) { - glyphCycleSpeed = fract((rainTime + 0.7 * sin(RADS_TO_HZ * SQRT_2 * rainTime) + 1.1 * sin(RADS_TO_HZ * SQRT_5 * rainTime))) * 0.75; - // glyphCycleSpeed = fract(rainTime) * 0.75; // loop - } - return glyphCycleSpeed; - } - - float applySunShower(float rainBrightness, vec2 screenPos) { - if (rainBrightness < -4.) { - return rainBrightness; - } - float value = pow(fract(rainBrightness * 0.5), 3.0) * screenPos.y * 1.5; - return value; - } - - float applyThunder(float rainBrightness, float simTime, vec2 screenPos) { - simTime *= 0.5; - float thunder = 1.0 - fract((simTime + 0.3 * sin(RADS_TO_HZ * SQRT_2 * simTime) + 0.2 * sin(RADS_TO_HZ * SQRT_5 * simTime))); - // thunder = 1.0 - fract(simTime + 0.3); // loop - thunder = log(thunder * 1.5) * 4.0; - thunder = clamp(thunder, 0., 1.); - thunder = thunder * pow(screenPos.y, 2.) * 3.; - return rainBrightness + thunder; - } - - float applyRippleEffect(float effect, float simTime, vec2 screenPos) { - if (rippleType == -1) { - return effect; - } - - float rippleTime = (simTime * 0.5 + 0.2 * sin(RADS_TO_HZ * simTime)) * rippleSpeed + 1.; - // rippleTime = (simTime * 0.5) * rippleSpeed + 1.; // loop - - vec2 offset = rand2(vec2(floor(rippleTime), 0.)) - 0.5; - // offset = vec2(0.); // loop - vec2 ripplePos = screenPos * 2.0 - 1.0 + offset; - float rippleDistance; - if (rippleType == 0) { - rippleDistance = max2(abs(ripplePos) * vec2(1.0, glyphHeightToWidth)); - } else if (rippleType == 1) { - rippleDistance = length(ripplePos); - } - - float rippleValue = fract(rippleTime) * rippleScale - rippleDistance; - - if (rippleValue > 0. && rippleValue < rippleThickness) { - return effect + 0.75; - } else { - return effect; - } - } - - float applyCursorEffect(float effect, float brightness) { - if (brightness >= cursorEffectThreshold) { - effect = 1.0; - } - return effect; - } - - void main() { - - vec2 glyphPos = gl_FragCoord.xy; - vec2 screenPos = glyphPos / vec2(numColumns, numRows); - float simTime = time * animationSpeed; - - // Read the current values of the glyph - vec4 data = texture2D( lastState, screenPos ); - bool isInitializing = length(data) == 0.; - float oldRainBrightness = data.r; - float oldGlyphCycle = data.g; - if (isInitializing) { - oldGlyphCycle = showComputationTexture ? 0.5 : rand(screenPos); - } - - if (oldRainBrightness <= 0.0) { - // oldGlyphCycle = showComputationTexture ? 0.5 : rand(screenPos); // loop - } - - float rainTime = getRainTime(simTime, glyphPos); - float rainBrightness = getRainBrightness(rainTime); - - if (hasSun) rainBrightness = applySunShower(rainBrightness, screenPos); - if (hasThunder) rainBrightness = applyThunder(rainBrightness, simTime, screenPos); - - float glyphCycleSpeed = getGlyphCycleSpeed(rainTime, rainBrightness); - float glyphCycle = fract(oldGlyphCycle + 0.005 * animationSpeed * cycleSpeed * glyphCycleSpeed); - - float effect = 0.; - effect = applyRippleEffect(effect, simTime, screenPos); - effect = applyCursorEffect(effect, rainBrightness); - - float glyphDepth = rand(vec2(glyphPos.x, 0.0)); - - if (rainBrightness > brightnessMinimum) { - rainBrightness = rainBrightness * brightnessMultiplier + brightnessOffset; - } - - if (!isInitializing) { - rainBrightness = mix(oldRainBrightness, rainBrightness, brightnessMix); - } - - if (showComputationTexture) { - gl_FragColor = vec4( - rainBrightness, - glyphCycle, - min(1.0, glyphCycleSpeed), // Better use of the blue channel, for show and tell - 1.0 - ); - } else { - gl_FragColor = vec4( - rainBrightness, - glyphCycle, - glyphDepth, - effect - ); - } - } - `, - + frag: regl.prop("frag"), uniforms: { ...uniforms, lastState: doubleBuffer.back @@ -294,6 +117,8 @@ export default (regl, config) => { const quadCorners = Array(numQuads).fill([[0, 0], [0, 1], [1, 1], [0, 0], [1, 1], [1, 0]]); // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen + const renderVert = loadText("../shaders/render.vert"); + const renderFrag = loadText("../shaders/render.frag"); const render = regl({ blend: { enable: true, @@ -304,152 +129,8 @@ export default (regl, config) => { dstAlpha: 1 } }, - vert: ` - #define PI 3.14159265359 - precision lowp float; - attribute vec2 aPosition, aCorner; - uniform sampler2D lastState; - uniform float density; - uniform vec2 quadSize; - uniform float glyphHeightToWidth; - uniform mat4 camera, transform; - uniform vec2 screenSize; - uniform float time, animationSpeed, forwardSpeed; - uniform bool volumetric; - uniform bool showComputationTexture; - uniform float resurrectingCodeRatio; - varying vec2 vUV; - varying vec3 vChannel; - varying vec4 vGlyph; - - highp float rand( const in vec2 uv ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract(sin(sn) * c); - } - - void main() { - - vUV = (aPosition + aCorner) * quadSize; - vGlyph = texture2D(lastState, aPosition * quadSize); - - float quadDepth = 0.0; - if (volumetric && !showComputationTexture) { - quadDepth = fract(vGlyph.b + time * animationSpeed * forwardSpeed); - vGlyph.b = quadDepth; - } - vec2 position = (aPosition + aCorner * vec2(density, 1.)) * quadSize; - vec4 pos = vec4((position - 0.5) * 2.0, quadDepth, 1.0); - - vChannel = vec3(1.0, 0.0, 0.0); - - if (volumetric) { - if (rand(vec2(aPosition.x, 0)) < resurrectingCodeRatio) { - pos.y = -pos.y; - vChannel = vec3(0.0, 1.0, 0.0); - } - - pos.x /= glyphHeightToWidth; - - pos = camera * transform * pos; - } else { - pos.xy *= screenSize; - } - - gl_Position = pos; - } - `, - - frag: ` - #define PI 3.14159265359 - #ifdef GL_OES_standard_derivatives - #extension GL_OES_standard_derivatives: enable - #endif - precision lowp float; - - uniform sampler2D lastState; - uniform float numColumns, numRows; - uniform sampler2D glyphTex; - uniform float glyphHeightToWidth, glyphSequenceLength, glyphTextureColumns, glyphEdgeCrop; - uniform vec2 slantVec; - uniform float slantScale; - uniform bool isPolar; - uniform bool showComputationTexture; - uniform bool volumetric; - - varying vec2 vUV; - varying vec3 vChannel; - varying vec4 vGlyph; - - float median3(vec3 i) { - return max(min(i.r, i.g), min(max(i.r, i.g), i.b)); - } - - float getSymbolIndex(float glyphCycle) { - float symbol = floor(glyphSequenceLength * glyphCycle); - float symbolX = mod(symbol, glyphTextureColumns); - float symbolY = ((glyphTextureColumns - 1.0) - (symbol - symbolX) / glyphTextureColumns); - return symbolY * glyphTextureColumns + symbolX; - } - - void main() { - - vec2 uv = vUV; - - if (!volumetric) { - if (isPolar) { - // Curves the UV space to make letters appear to radiate from up above - uv -= 0.5; - uv *= 0.5; - uv.y -= 0.5; - float radius = length(uv); - float angle = atan(uv.y, uv.x) / (2. * PI) + 0.5; - uv = vec2(angle * 4. - 0.5, 1.5 - pow(radius, 0.5) * 1.5); - } else { - // Applies the slant, scaling the UV space - // to guarantee the viewport is still covered - uv = vec2( - (uv.x - 0.5) * slantVec.x + (uv.y - 0.5) * slantVec.y, - (uv.y - 0.5) * slantVec.x - (uv.x - 0.5) * slantVec.y - ) * slantScale + 0.5; - } - uv.y /= glyphHeightToWidth; - } - - vec4 glyph = volumetric ? vGlyph : texture2D(lastState, uv); - - if (showComputationTexture) { - gl_FragColor = glyph; - return; - } - - // Unpack the values from the font texture - float brightness = glyph.r; - float symbolIndex = getSymbolIndex(glyph.g); - float quadDepth = glyph.b; - float effect = glyph.a; - - brightness = max(effect, brightness); - if (volumetric) { - brightness = min(1.0, brightness * quadDepth * 1.25); - } - - // resolve UV to MSDF texture coord - vec2 symbolUV = vec2(mod(symbolIndex, glyphTextureColumns), floor(symbolIndex / glyphTextureColumns)); - vec2 glyphUV = fract(uv * vec2(numColumns, numRows)); - glyphUV -= 0.5; - glyphUV *= clamp(1.0 - glyphEdgeCrop, 0.0, 1.0); - glyphUV += 0.5; - vec2 msdfUV = (glyphUV + symbolUV) / glyphTextureColumns; - - // MSDF - vec3 dist = texture2D(glyphTex, msdfUV).rgb; - float sigDist = median3(dist) - 0.5; - float alpha = clamp(sigDist/fwidth(sigDist) + 0.5, 0.0, 1.0); - - gl_FragColor = vec4(vChannel * brightness * alpha, 1.0); - } - `, + vert: regl.prop("vert"), + frag: regl.prop("frag"), uniforms: { ...uniforms, @@ -487,13 +168,13 @@ export default (regl, config) => { () => { const time = Date.now(); - update(); + update({frag: updateFrag.text()}); regl.clear({ depth: 1, color: [0, 0, 0, 1], framebuffer: output }); - render({camera, transform, screenSize}); + render({camera, transform, screenSize, vert: renderVert.text(), frag: renderFrag.text()}); }, (w, h) => { output.resize(w, h); @@ -501,6 +182,6 @@ export default (regl, config) => { glMatrix.mat4.perspective(camera, (Math.PI / 180) * 90, aspectRatio, 0.0001, 1000); [screenSize[0], screenSize[1]] = aspectRatio > 1 ? [1, aspectRatio] : [1 / aspectRatio, 1]; }, - msdf.loaded + [msdf.loaded, updateFrag.loaded, renderVert.loaded, renderFrag.loaded] ); }; diff --git a/js/resurrectionPass.js b/js/resurrectionPass.js index f45231b..fd84d10 100644 --- a/js/resurrectionPass.js +++ b/js/resurrectionPass.js @@ -1,4 +1,4 @@ -import { extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; +import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; const colorToRGB = ([hue, saturation, lightness]) => { const a = saturation * Math.min(lightness, 1 - lightness); @@ -12,67 +12,26 @@ const colorToRGB = ([hue, saturation, lightness]) => { export default (regl, config, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); + const resurrectionPassFrag = loadText("../shaders/resurrectionPass.frag"); + + const render = regl({ + frag: regl.prop("frag"), + + uniforms: { + ...extractEntries(config, [ + "backgroundColor", + ]), + tex: inputs.primary, + bloomTex: inputs.bloom, + ditherMagnitude: 0.05 + }, + framebuffer: output + }); + return makePass( { primary: output }, - regl({ - frag: ` - precision mediump float; - #define PI 3.14159265359 - - uniform sampler2D tex; - uniform sampler2D bloomTex; - uniform float ditherMagnitude; - uniform float time; - uniform vec3 backgroundColor; - varying vec2 vUV; - - highp float rand( const in vec2 uv, const in float t ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract(sin(sn) * c + t); - } - - float rgbComponent(float p, float q, float t) { - if (t < 0.0) t += 1.0; - if (t > 1.0) t -= 1.0; - if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t; - if (t < 1.0 / 2.0) return q; - if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0; - return p; - } - - vec3 hslToRgb(float h, float s, float l){ - float q = l < 0.5 ? l * (1. + s) : l + s - l * s; - float p = 2.0 * l - q; - return vec3( - rgbComponent(p, q, h + 1.0 / 3.0), - rgbComponent(p, q, h), - rgbComponent(p, q, h - 1.0 / 3.0) - ); - } - - void main() { - - vec3 brightness = mix(texture2D( bloomTex, vUV ).rgb, texture2D( tex, vUV ).rgb, (0.7 - length(vUV - 0.5))) * 1.25 - rand( gl_FragCoord.xy, time ) * ditherMagnitude; - - float hue = 0.35 + (length(vUV - vec2(0.5, 1.0)) * -0.4 + 0.2); - vec3 rgb = hslToRgb(hue, 0.8, max(0., brightness.r)) * vec3(0.8, 1.0, 0.7); - vec3 resurrectionRGB = hslToRgb(0.13, 1.0, max(0., brightness.g) * 0.9); - gl_FragColor = vec4(rgb + resurrectionRGB + backgroundColor, 1.0); - } - `, - - uniforms: { - ...extractEntries(config, [ - "backgroundColor", - ]), - tex: inputs.primary, - bloomTex: inputs.bloom, - ditherMagnitude: 0.05 - }, - framebuffer: output - }) + () => render({frag: resurrectionPassFrag.text() }) ); }; diff --git a/js/stripePass.js b/js/stripePass.js index 715a1bf..d68d57f 100644 --- a/js/stripePass.js +++ b/js/stripePass.js @@ -1,4 +1,4 @@ -import { extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; +import { loadText, extractEntries, make1DTexture, makePassFBO, makePass } from "./utils.js"; const neapolitanStripeColors = [ [0.4, 0.15, 0.1], @@ -33,47 +33,29 @@ export default (regl, config, inputs) => { stripeColors.slice(0, numStripeColors * 3).map(f => Math.floor(f * 0xff)) ); + const stripePassFrag = loadText("../shaders/stripePass.frag"); + + const render = regl({ + frag: regl.prop("frag"), + + uniforms: { + ...extractEntries(config, [ + "backgroundColor", + ]), + tex: inputs.primary, + bloomTex: inputs.bloom, + stripes, + ditherMagnitude: 0.05 + }, + framebuffer: output + }); + return makePass( { primary: output }, - regl({ - frag: ` - precision mediump float; - #define PI 3.14159265359 - - uniform sampler2D tex; - uniform sampler2D bloomTex; - uniform sampler2D stripes; - uniform float ditherMagnitude; - uniform float time; - uniform vec3 backgroundColor; - varying vec2 vUV; - - highp float rand( const in vec2 uv, const in float t ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract(sin(sn) * c + t); - } - - void main() { - vec3 color = texture2D(stripes, vUV).rgb; - float brightness = min(1., texture2D(tex, vUV).r * 2.) + texture2D(bloomTex, vUV).r; - float at = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude; - gl_FragColor = vec4(color * at + backgroundColor, 1.0); - } - `, - - uniforms: { - ...extractEntries(config, [ - "backgroundColor", - ]), - tex: inputs.primary, - bloomTex: inputs.bloom, - stripes, - ditherMagnitude: 0.05 - }, - framebuffer: output - }) + () => render({frag: stripePassFrag.text()}), + null, + stripePassFrag.loaded ); }; diff --git a/js/utils.js b/js/utils.js index f44581d..288a69d 100644 --- a/js/utils.js +++ b/js/utils.js @@ -177,6 +177,8 @@ const makePass = (outputs, render, resize, ready) => { } if (ready == null) { ready = Promise.resolve(); + } else if (ready instanceof Array) { + ready = Promise.all(ready); } return { outputs, diff --git a/shaders/blur.frag b/shaders/blur.frag new file mode 100644 index 0000000..b38792f --- /dev/null +++ b/shaders/blur.frag @@ -0,0 +1,24 @@ +precision mediump float; +uniform float width, height; +uniform sampler2D tex; +uniform vec2 direction; +varying vec2 vUV; +void main() { + vec2 size = width > height ? vec2(width / height, 1.) : vec2(1., height / width); + gl_FragColor = + texture2D(tex, vUV) * 0.442 + + ( + texture2D(tex, vUV + direction / max(width, height) * size) + + texture2D(tex, vUV - direction / max(width, height) * size) + ) * 0.279; + // gl_FragColor = + // texture2D(tex, vUV) * 0.38774 + + // ( + // texture2D(tex, vUV + direction / max(width, height) * size * 0.5) + + // texture2D(tex, vUV - direction / max(width, height) * size * 0.5) + // ) * 0.24477 + + // ( + // texture2D(tex, vUV + direction / max(width, height) * size) + + // texture2D(tex, vUV - direction / max(width, height) * size) + // ) * 0.06136; +} diff --git a/shaders/highPass.frag b/shaders/highPass.frag new file mode 100644 index 0000000..ca26c30 --- /dev/null +++ b/shaders/highPass.frag @@ -0,0 +1,11 @@ +precision mediump float; +varying vec2 vUV; +uniform sampler2D tex; +uniform float highPassThreshold; +void main() { + vec3 lumaColor = texture2D(tex, vUV).rgb; + if (lumaColor.r < highPassThreshold) lumaColor.r = 0.0; + if (lumaColor.g < highPassThreshold) lumaColor.g = 0.0; + if (lumaColor.b < highPassThreshold) lumaColor.b = 0.0; + gl_FragColor = vec4(lumaColor, 1.0); +} diff --git a/shaders/imagePass.frag b/shaders/imagePass.frag new file mode 100644 index 0000000..ea66d38 --- /dev/null +++ b/shaders/imagePass.frag @@ -0,0 +1,11 @@ +precision mediump float; +uniform sampler2D tex; +uniform sampler2D bloomTex; +uniform sampler2D backgroundTex; +varying vec2 vUV; + +void main() { + vec3 bgColor = texture2D(backgroundTex, vUV).rgb; + float brightness = pow(min(1., texture2D(tex, vUV).r * 2.) + texture2D(bloomTex, vUV).r, 1.5); + gl_FragColor = vec4(bgColor * brightness, 1.0); +} diff --git a/shaders/palettePass.frag b/shaders/palettePass.frag new file mode 100644 index 0000000..71aacec --- /dev/null +++ b/shaders/palettePass.frag @@ -0,0 +1,23 @@ +precision mediump float; +#define PI 3.14159265359 + +uniform sampler2D tex; +uniform sampler2D bloomTex; +uniform sampler2D palette; +uniform float ditherMagnitude; +uniform float time; +uniform vec3 backgroundColor; +varying vec2 vUV; + +highp float rand( const in vec2 uv, const in float t ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract(sin(sn) * c + t); +} + +void main() { + vec4 brightnessRGB = texture2D( tex, vUV ) + texture2D( bloomTex, vUV ); + float brightness = brightnessRGB.r + brightnessRGB.g + brightnessRGB.b; + float at = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude; + gl_FragColor = texture2D( palette, vec2(at, 0.0)) + vec4(backgroundColor, 0.0); +} diff --git a/shaders/render.frag b/shaders/render.frag new file mode 100644 index 0000000..751e1d2 --- /dev/null +++ b/shaders/render.frag @@ -0,0 +1,88 @@ +#define PI 3.14159265359 +#ifdef GL_OES_standard_derivatives +#extension GL_OES_standard_derivatives: enable +#endif +precision lowp float; + +uniform sampler2D lastState; +uniform float numColumns, numRows; +uniform sampler2D glyphTex; +uniform float glyphHeightToWidth, glyphSequenceLength, glyphTextureColumns, glyphEdgeCrop; +uniform vec2 slantVec; +uniform float slantScale; +uniform bool isPolar; +uniform bool showComputationTexture; +uniform bool volumetric; + +varying vec2 vUV; +varying vec3 vChannel; +varying vec4 vGlyph; + +float median3(vec3 i) { + return max(min(i.r, i.g), min(max(i.r, i.g), i.b)); +} + +float getSymbolIndex(float glyphCycle) { + float symbol = floor(glyphSequenceLength * glyphCycle); + float symbolX = mod(symbol, glyphTextureColumns); + float symbolY = ((glyphTextureColumns - 1.0) - (symbol - symbolX) / glyphTextureColumns); + return symbolY * glyphTextureColumns + symbolX; +} + +void main() { + + vec2 uv = vUV; + + if (!volumetric) { + if (isPolar) { + // Curves the UV space to make letters appear to radiate from up above + uv -= 0.5; + uv *= 0.5; + uv.y -= 0.5; + float radius = length(uv); + float angle = atan(uv.y, uv.x) / (2. * PI) + 0.5; + uv = vec2(angle * 4. - 0.5, 1.5 - pow(radius, 0.5) * 1.5); + } else { + // Applies the slant, scaling the UV space + // to guarantee the viewport is still covered + uv = vec2( + (uv.x - 0.5) * slantVec.x + (uv.y - 0.5) * slantVec.y, + (uv.y - 0.5) * slantVec.x - (uv.x - 0.5) * slantVec.y + ) * slantScale + 0.5; + } + uv.y /= glyphHeightToWidth; + } + + vec4 glyph = volumetric ? vGlyph : texture2D(lastState, uv); + + if (showComputationTexture) { + gl_FragColor = glyph; + return; + } + + // Unpack the values from the font texture + float brightness = glyph.r; + float symbolIndex = getSymbolIndex(glyph.g); + float quadDepth = glyph.b; + float effect = glyph.a; + + brightness = max(effect, brightness); + if (volumetric) { + brightness = min(1.0, brightness * quadDepth * 1.25); + } + + // resolve UV to MSDF texture coord + vec2 symbolUV = vec2(mod(symbolIndex, glyphTextureColumns), floor(symbolIndex / glyphTextureColumns)); + vec2 glyphUV = fract(uv * vec2(numColumns, numRows)); + glyphUV -= 0.5; + glyphUV *= clamp(1.0 - glyphEdgeCrop, 0.0, 1.0); + glyphUV += 0.5; + vec2 msdfUV = (glyphUV + symbolUV) / glyphTextureColumns; + + // MSDF + vec3 dist = texture2D(glyphTex, msdfUV).rgb; + float sigDist = median3(dist) - 0.5; + float alpha = clamp(sigDist/fwidth(sigDist) + 0.5, 0.0, 1.0); + + gl_FragColor = vec4(vChannel * brightness * alpha, 1.0); +} diff --git a/shaders/render.vert b/shaders/render.vert new file mode 100644 index 0000000..9784065 --- /dev/null +++ b/shaders/render.vert @@ -0,0 +1,53 @@ +#define PI 3.14159265359 +precision lowp float; +attribute vec2 aPosition, aCorner; +uniform sampler2D lastState; +uniform float density; +uniform vec2 quadSize; +uniform float glyphHeightToWidth; +uniform mat4 camera, transform; +uniform vec2 screenSize; +uniform float time, animationSpeed, forwardSpeed; +uniform bool volumetric; +uniform bool showComputationTexture; +uniform float resurrectingCodeRatio; +varying vec2 vUV; +varying vec3 vChannel; +varying vec4 vGlyph; + +highp float rand( const in vec2 uv ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract(sin(sn) * c); +} + +void main() { + + vUV = (aPosition + aCorner) * quadSize; + vGlyph = texture2D(lastState, aPosition * quadSize); + + float quadDepth = 0.0; + if (volumetric && !showComputationTexture) { + quadDepth = fract(vGlyph.b + time * animationSpeed * forwardSpeed); + vGlyph.b = quadDepth; + } + vec2 position = (aPosition + aCorner * vec2(density, 1.)) * quadSize; + vec4 pos = vec4((position - 0.5) * 2.0, quadDepth, 1.0); + + vChannel = vec3(1.0, 0.0, 0.0); + + if (volumetric) { + if (rand(vec2(aPosition.x, 0)) < resurrectingCodeRatio) { + pos.y = -pos.y; + vChannel = vec3(0.0, 1.0, 0.0); + } + + pos.x /= glyphHeightToWidth; + + pos = camera * transform * pos; + } else { + pos.xy *= screenSize; + } + + gl_Position = pos; +} diff --git a/shaders/resurrectionPass.frag b/shaders/resurrectionPass.frag new file mode 100644 index 0000000..373dd34 --- /dev/null +++ b/shaders/resurrectionPass.frag @@ -0,0 +1,44 @@ +precision mediump float; +#define PI 3.14159265359 + +uniform sampler2D tex; +uniform sampler2D bloomTex; +uniform float ditherMagnitude; +uniform float time; +uniform vec3 backgroundColor; +varying vec2 vUV; + +highp float rand( const in vec2 uv, const in float t ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract(sin(sn) * c + t); +} + +float rgbComponent(float p, float q, float t) { + if (t < 0.0) t += 1.0; + if (t > 1.0) t -= 1.0; + if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t; + if (t < 1.0 / 2.0) return q; + if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0; + return p; +} + +vec3 hslToRgb(float h, float s, float l){ + float q = l < 0.5 ? l * (1. + s) : l + s - l * s; + float p = 2.0 * l - q; + return vec3( + rgbComponent(p, q, h + 1.0 / 3.0), + rgbComponent(p, q, h), + rgbComponent(p, q, h - 1.0 / 3.0) + ); +} + +void main() { + + vec3 brightness = mix(texture2D( bloomTex, vUV ).rgb, texture2D( tex, vUV ).rgb, (0.7 - length(vUV - 0.5))) * 1.25 - rand( gl_FragCoord.xy, time ) * ditherMagnitude; + + float hue = 0.35 + (length(vUV - vec2(0.5, 1.0)) * -0.4 + 0.2); + vec3 rgb = hslToRgb(hue, 0.8, max(0., brightness.r)) * vec3(0.8, 1.0, 0.7); + vec3 resurrectionRGB = hslToRgb(0.13, 1.0, max(0., brightness.g) * 0.9); + gl_FragColor = vec4(rgb + resurrectionRGB + backgroundColor, 1.0); +} diff --git a/shaders/stripePass.frag b/shaders/stripePass.frag new file mode 100644 index 0000000..10c1076 --- /dev/null +++ b/shaders/stripePass.frag @@ -0,0 +1,23 @@ +precision mediump float; +#define PI 3.14159265359 + +uniform sampler2D tex; +uniform sampler2D bloomTex; +uniform sampler2D stripes; +uniform float ditherMagnitude; +uniform float time; +uniform vec3 backgroundColor; +varying vec2 vUV; + +highp float rand( const in vec2 uv, const in float t ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract(sin(sn) * c + t); +} + +void main() { + vec3 color = texture2D(stripes, vUV).rgb; + float brightness = min(1., texture2D(tex, vUV).r * 2.) + texture2D(bloomTex, vUV).r; + float at = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude; + gl_FragColor = vec4(color * at + backgroundColor, 1.0); +} diff --git a/shaders/update.frag b/shaders/update.frag new file mode 100644 index 0000000..9965a26 --- /dev/null +++ b/shaders/update.frag @@ -0,0 +1,178 @@ +precision highp float; + +// This shader is the star of the show. +// In normal operation, each pixel represents a glyph's: +// R: brightness +// G: progress through the glyph sequence +// B: current glyph index +// A: additional brightness, for effects + +#define PI 3.14159265359 +#define RADS_TO_HZ 0.15915494309 +#define SQRT_2 1.4142135623730951 +#define SQRT_5 2.23606797749979 + +uniform float time; +uniform float numColumns, numRows; +uniform sampler2D lastState; +uniform bool hasSun; +uniform bool hasThunder; +uniform bool showComputationTexture; +uniform float brightnessMinimum, brightnessMultiplier, brightnessOffset, brightnessMix; +uniform float animationSpeed, fallSpeed, cycleSpeed; +uniform float raindropLength; +uniform float glyphHeightToWidth; +uniform int cycleStyle; +uniform float rippleScale, rippleSpeed, rippleThickness; +uniform int rippleType; +uniform float cursorEffectThreshold; + +float max2(vec2 v) { + return max(v.x, v.y); +} + +highp float rand( const in vec2 uv ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract(sin(sn) * c); +} + +vec2 rand2(vec2 p) { + return fract(vec2(sin(p.x * 591.32 + p.y * 154.077), cos(p.x * 391.32 + p.y * 49.077))); +} + +float getRainTime(float simTime, vec2 glyphPos) { + float columnTimeOffset = rand(vec2(glyphPos.x, 0.0)); + float columnSpeedOffset = rand(vec2(glyphPos.x + 0.1, 0.0)); + // columnSpeedOffset = 0.0; // loop + float columnTime = (columnTimeOffset * 1000.0 + simTime * 0.5 * fallSpeed) * (0.5 + columnSpeedOffset * 0.5) + (sin(RADS_TO_HZ * simTime * fallSpeed * columnSpeedOffset) * 0.2); + return (glyphPos.y * 0.01 + columnTime) / raindropLength; +} + +float getRainBrightness(float rainTime) { + float value = 1.0 - fract((rainTime + 0.3 * sin(RADS_TO_HZ * SQRT_2 * rainTime) + 0.2 * sin(RADS_TO_HZ * SQRT_5 * rainTime))); + // value = 1.0 - fract(rainTime); // loop + return log(value * 1.25) * 3.0; +} + +float getGlyphCycleSpeed(float rainTime, float brightness) { + float glyphCycleSpeed = 0.0; + if (cycleStyle == 0 && brightness > 0.0) { + glyphCycleSpeed = pow(1.0 - brightness, 4.0); + } else if (cycleStyle == 1) { + glyphCycleSpeed = fract((rainTime + 0.7 * sin(RADS_TO_HZ * SQRT_2 * rainTime) + 1.1 * sin(RADS_TO_HZ * SQRT_5 * rainTime))) * 0.75; + // glyphCycleSpeed = fract(rainTime) * 0.75; // loop + } + return glyphCycleSpeed; +} + +float applySunShower(float rainBrightness, vec2 screenPos) { + if (rainBrightness < -4.) { + return rainBrightness; + } + float value = pow(fract(rainBrightness * 0.5), 3.0) * screenPos.y * 1.5; + return value; +} + +float applyThunder(float rainBrightness, float simTime, vec2 screenPos) { + simTime *= 0.5; + float thunder = 1.0 - fract((simTime + 0.3 * sin(RADS_TO_HZ * SQRT_2 * simTime) + 0.2 * sin(RADS_TO_HZ * SQRT_5 * simTime))); + // thunder = 1.0 - fract(simTime + 0.3); // loop + thunder = log(thunder * 1.5) * 4.0; + thunder = clamp(thunder, 0., 1.); + thunder = thunder * pow(screenPos.y, 2.) * 3.; + return rainBrightness + thunder; +} + +float applyRippleEffect(float effect, float simTime, vec2 screenPos) { + if (rippleType == -1) { + return effect; + } + + float rippleTime = (simTime * 0.5 + 0.2 * sin(RADS_TO_HZ * simTime)) * rippleSpeed + 1.; + // rippleTime = (simTime * 0.5) * rippleSpeed + 1.; // loop + + vec2 offset = rand2(vec2(floor(rippleTime), 0.)) - 0.5; + // offset = vec2(0.); // loop + vec2 ripplePos = screenPos * 2.0 - 1.0 + offset; + float rippleDistance; + if (rippleType == 0) { + rippleDistance = max2(abs(ripplePos) * vec2(1.0, glyphHeightToWidth)); + } else if (rippleType == 1) { + rippleDistance = length(ripplePos); + } + + float rippleValue = fract(rippleTime) * rippleScale - rippleDistance; + + if (rippleValue > 0. && rippleValue < rippleThickness) { + return effect + 0.75; + } else { + return effect; + } +} + +float applyCursorEffect(float effect, float brightness) { + if (brightness >= cursorEffectThreshold) { + effect = 1.0; + } + return effect; +} + +void main() { + + vec2 glyphPos = gl_FragCoord.xy; + vec2 screenPos = glyphPos / vec2(numColumns, numRows); + float simTime = time * animationSpeed; + + // Read the current values of the glyph + vec4 data = texture2D( lastState, screenPos ); + bool isInitializing = length(data) == 0.; + float oldRainBrightness = data.r; + float oldGlyphCycle = data.g; + if (isInitializing) { + oldGlyphCycle = showComputationTexture ? 0.5 : rand(screenPos); + } + + if (oldRainBrightness <= 0.0) { + // oldGlyphCycle = showComputationTexture ? 0.5 : rand(screenPos); // loop + } + + float rainTime = getRainTime(simTime, glyphPos); + float rainBrightness = getRainBrightness(rainTime); + + if (hasSun) rainBrightness = applySunShower(rainBrightness, screenPos); + if (hasThunder) rainBrightness = applyThunder(rainBrightness, simTime, screenPos); + + float glyphCycleSpeed = getGlyphCycleSpeed(rainTime, rainBrightness); + float glyphCycle = fract(oldGlyphCycle + 0.005 * animationSpeed * cycleSpeed * glyphCycleSpeed); + + float effect = 0.; + effect = applyRippleEffect(effect, simTime, screenPos); + effect = applyCursorEffect(effect, rainBrightness); + + float glyphDepth = rand(vec2(glyphPos.x, 0.0)); + + if (rainBrightness > brightnessMinimum) { + rainBrightness = rainBrightness * brightnessMultiplier + brightnessOffset; + } + + if (!isInitializing) { + rainBrightness = mix(oldRainBrightness, rainBrightness, brightnessMix); + } + + if (showComputationTexture) { + gl_FragColor = vec4( + rainBrightness, + glyphCycle, + min(1.0, glyphCycleSpeed), // Better use of the blue channel, for show and tell + 1.0 + ); + } else { + gl_FragColor = vec4( + rainBrightness, + glyphCycle, + glyphDepth, + effect + ); + } +}