diff --git a/TODO.txt b/TODO.txt index 01cfbb8..2832718 100644 --- a/TODO.txt +++ b/TODO.txt @@ -1,6 +1,6 @@ TODO: -Migrate recoded changes to WebGPU +Isolate classic from defaults Update the README @@ -67,18 +67,18 @@ Write an explanation of the rain pass (and include images) Fullscreen quad and spacial mapping MSDFs +Idea: Build a UI + Replace versions with presets + Simple changes update the values + Complex changes replace the pipeline + Make it a form, so it's accessible + Then, make it look cool like the UI from the old site + +Maybe pay someone to make Mac/Windows screensavers + Zion Control's matrix variant From Reloaded -Idea: Build a UI - Changes some uniforms - Effect selection would swap out the desired effect pass, somehow - The color palette stuff would be hard - -Deja vu effect: flashing rows - Make them flash all the time - Then use a thunder-like pattern to show and hide the flash - gpu-buffer, working title Support type aliasing (type Q = array) Support shorthand (vec4f) diff --git a/index.html b/index.html index 2481b88..533a71d 100644 --- a/index.html +++ b/index.html @@ -23,7 +23,7 @@ This project demonstrates five concepts: 1. Drawing to floating point frame buffer objects, or 'FBO's, for performing computation and post-processing - 2. GPU-side computation, with a fragment shader + 2. GPU-side computation, with fragment shaders updating two alternating FBOs 3. Rendering crisp "vector" graphics, with a multiple-channel signed distance field (or 'MSDF') diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js index e6ac123..126c1b6 100644 --- a/js/webgpu/palettePass.js +++ b/js/webgpu/palettePass.js @@ -107,6 +107,7 @@ export default ({ config, device, timeBuffer }) => { bloomStrength: config.bloomStrength, ditherMagnitude: config.ditherMagnitude, backgroundColor: config.backgroundColor, + cursorColor: config.cursorColor, }); const paletteUniforms = paletteShaderUniforms.Palette; diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index 8a11f93..d43023e 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -6,11 +6,6 @@ const rippleTypes = { circle: 1, }; -const cycleStyles = { - cycleFasterWhenDimmed: 0, - cycleRandomly: 1, -}; - const numVerticesPerQuad = 2 * 3; const makeConfigBuffer = (device, configUniforms, config, density, gridSize) => { @@ -19,7 +14,6 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize) => gridSize, density, showDebugView: config.effect === "none", - cycleStyle: config.cycleStyleName in cycleStyles ? cycleStyles[config.cycleStyleName] : 0, rippleType: config.rippleTypeName in rippleTypes ? rippleTypes[config.rippleTypeName] : -1, slantScale: 1 / (Math.abs(Math.sin(2 * config.slant)) * (Math.sqrt(2) - 1) + 1), slantVec: [Math.cos(config.slant), Math.sin(config.slant)], diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js index b83123e..64467d4 100644 --- a/js/webgpu/stripePass.js +++ b/js/webgpu/stripePass.js @@ -78,6 +78,7 @@ export default ({ config, device, timeBuffer }) => { bloomStrength: config.bloomStrength, ditherMagnitude: config.ditherMagnitude, backgroundColor: config.backgroundColor, + cursorColor: config.cursorColor, }); })(); diff --git a/shaders/glsl/palettePass.frag.glsl b/shaders/glsl/palettePass.frag.glsl index e636e9d..b2d5322 100644 --- a/shaders/glsl/palettePass.frag.glsl +++ b/shaders/glsl/palettePass.frag.glsl @@ -23,18 +23,15 @@ vec4 getBrightness(vec2 uv) { } void main() { - vec4 brightnessRGB = getBrightness(vUV); - - // Combine the texture and bloom - vec2 brightness = brightnessRGB.rg; + vec4 brightness = getBrightness(vUV); // Dither: subtract a random value from the brightness - brightness = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude; + brightness -= rand( gl_FragCoord.xy, time ) * ditherMagnitude; // Map the brightness to a position in the palette texture gl_FragColor = vec4( texture2D( palette, vec2(brightness.r, 0.0)).rgb - + min(cursorColor * brightness.g, 1.0) + + min(cursorColor * brightness.g, vec3(1.0)) + backgroundColor, 1.0 ); diff --git a/shaders/glsl/rainPass.effect.frag.glsl b/shaders/glsl/rainPass.effect.frag.glsl index 32004d6..b100448 100644 --- a/shaders/glsl/rainPass.effect.frag.glsl +++ b/shaders/glsl/rainPass.effect.frag.glsl @@ -35,10 +35,10 @@ float getThunder(float simTime, vec2 screenPos) { return 0.; } - simTime *= 0.5; - float thunder = 1. - fract(wobble(simTime)); + float thunderTime = simTime * 0.5; + float thunder = 1. - fract(wobble(thunderTime)); if (loops) { - thunder = 1. - fract(simTime + 0.3); + thunder = 1. - fract(thunderTime + 0.3); } thunder = log(thunder * 1.5) * 4.; @@ -82,7 +82,7 @@ float getRipple(float simTime, vec2 screenPos) { vec4 computeResult(float simTime, bool isFirstFrame, vec2 glyphPos, vec2 screenPos, vec4 previous) { - float multipliedEffects = 1.0 + getThunder(simTime, screenPos); + float multipliedEffects = 1. + getThunder(simTime, screenPos); float addedEffects = getRipple(simTime, screenPos); // Round or square ripples across the grid vec4 result = vec4(multipliedEffects, addedEffects, 0., 0.); diff --git a/shaders/glsl/rainPass.frag.glsl b/shaders/glsl/rainPass.frag.glsl index 867a741..0b8e138 100644 --- a/shaders/glsl/rainPass.frag.glsl +++ b/shaders/glsl/rainPass.frag.glsl @@ -19,7 +19,7 @@ uniform bool volumetric; uniform bool isolateCursor; varying vec2 vUV; -varying vec4 vShine, vSymbol, vEffect; +varying vec4 vRaindrop, vSymbol, vEffect; varying float vDepth; float median3(vec3 i) { @@ -62,7 +62,7 @@ vec2 getBrightness(float brightness, float cursor, float multipliedEffects, floa if (!isolateCursor) { cursor = 0.; } - brightness = (1.0 - brightness) * baseContrast + baseBrightness; + brightness = (1. - brightness) * baseContrast + baseBrightness; // Modes that don't fade glyphs set their actual brightness here if (brightnessOverride > 0. && brightness > brightnessThreshold && cursor == 0.) { @@ -98,7 +98,7 @@ float getSymbol(vec2 uv, float index) { // MSDF: calculate brightness of fragment based on distance to shape vec3 dist = texture2D(glyphTex, uv).rgb; float sigDist = median3(dist) - 0.5; - return clamp(sigDist/fwidth(sigDist) + 0.5, 0., 1.); + return clamp(sigDist / fwidth(sigDist) + 0.5, 0., 1.); } void main() { @@ -106,9 +106,9 @@ void main() { vec2 uv = getUV(vUV); // Unpack the values from the data textures - vec4 raindropData = volumetric ? vShine : texture2D( raindropState, uv); - vec4 symbolData = volumetric ? vSymbol : texture2D(symbolState, uv); - vec4 effectData = volumetric ? vEffect : texture2D(effectState, uv); + vec4 raindropData = volumetric ? vRaindrop : texture2D(raindropState, uv); + vec4 symbolData = volumetric ? vSymbol : texture2D( symbolState, uv); + vec4 effectData = volumetric ? vEffect : texture2D( effectState, uv); vec2 brightness = getBrightness(raindropData.r, raindropData.g, effectData.r, effectData.g); float symbol = getSymbol(uv, symbolData.r); @@ -118,9 +118,9 @@ void main() { vec3( raindropData.g, vec2( - 1.0 - (raindropData.r * 3.0), - 1.0 - (raindropData.r * 8.0) - ) * (1.0 - raindropData.g) + 1. - (raindropData.r * 3.), + 1. - (raindropData.r * 8.) + ) * (1. - raindropData.g) ) * symbol, 1. ); diff --git a/shaders/glsl/rainPass.raindrop.frag.glsl b/shaders/glsl/rainPass.raindrop.frag.glsl index df884e7..db2db56 100644 --- a/shaders/glsl/rainPass.raindrop.frag.glsl +++ b/shaders/glsl/rainPass.raindrop.frag.glsl @@ -2,9 +2,9 @@ precision highp float; // This shader is the star of the show. // It writes falling rain to the channels of a data texture: -// R: brightness -// G: unused -// B: whether the cell is a "cursor" +// R: raindrop brightness +// G: whether the cell is a "cursor" +// B: unused // A: unused // Listen. diff --git a/shaders/glsl/rainPass.vert.glsl b/shaders/glsl/rainPass.vert.glsl index 04a00ff..e11d5d8 100644 --- a/shaders/glsl/rainPass.vert.glsl +++ b/shaders/glsl/rainPass.vert.glsl @@ -10,7 +10,7 @@ uniform vec2 screenSize; uniform float time, animationSpeed, forwardSpeed; uniform bool volumetric; varying vec2 vUV; -varying vec4 vShine, vSymbol, vEffect; +varying vec4 vRaindrop, vSymbol, vEffect; varying float vDepth; highp float rand( const in vec2 uv ) { @@ -22,9 +22,9 @@ highp float rand( const in vec2 uv ) { void main() { vUV = (aPosition + aCorner) * quadSize; - vShine = texture2D(raindropState, aPosition * quadSize); - vSymbol = texture2D(symbolState, aPosition * quadSize); - vEffect = texture2D(effectState, aPosition * quadSize); + vRaindrop = texture2D(raindropState, aPosition * quadSize); + vSymbol = texture2D( symbolState, aPosition * quadSize); + vEffect = texture2D( effectState, aPosition * quadSize); // Calculate the world space position float quadDepth = 0.0; diff --git a/shaders/glsl/stripePass.frag.glsl b/shaders/glsl/stripePass.frag.glsl index 6290392..18ea0ac 100644 --- a/shaders/glsl/stripePass.frag.glsl +++ b/shaders/glsl/stripePass.frag.glsl @@ -24,11 +24,11 @@ vec4 getBrightness(vec2 uv) { void main() { vec3 color = texture2D(stripes, vUV).rgb; - // Combine the texture and bloom + vec4 brightness = getBrightness(vUV); // Dither: subtract a random value from the brightness - brightness = brightness - rand( gl_FragCoord.xy, time ) * ditherMagnitude; + brightness -= rand( gl_FragCoord.xy, time ) * ditherMagnitude; gl_FragColor = vec4( color * brightness.r diff --git a/shaders/wgsl/imagePass.wgsl b/shaders/wgsl/imagePass.wgsl index 16f6adf..8aeb477 100644 --- a/shaders/wgsl/imagePass.wgsl +++ b/shaders/wgsl/imagePass.wgsl @@ -34,8 +34,7 @@ fn getBrightness(uv : vec2) -> vec4 { var bgColor = textureSampleLevel( backgroundTex, linearSampler, uv, 0.0 ).rgb; // Combine the texture and bloom, then blow it out to reveal more of the image - var brightness = getBrightness(uv).r; - brightness = pow(brightness, 1.5); + var brightness = getBrightness(uv); - textureStore(outputTex, coord, vec4(bgColor * brightness, 1.0)); + textureStore(outputTex, coord, vec4(bgColor * (brightness.r + brightness.g * 2.0), 1.0)); } diff --git a/shaders/wgsl/palettePass.wgsl b/shaders/wgsl/palettePass.wgsl index 4b78f2e..06f01ee 100644 --- a/shaders/wgsl/palettePass.wgsl +++ b/shaders/wgsl/palettePass.wgsl @@ -2,6 +2,7 @@ struct Config { bloomStrength : f32, ditherMagnitude : f32, backgroundColor : vec3, + cursorColor : vec3 }; struct Palette { @@ -54,17 +55,19 @@ fn getBrightness(uv : vec2) -> vec4 { var uv = vec2(coord) / vec2(screenSize); - var brightnessRGB = getBrightness(uv); - - // Combine the texture and bloom - var brightness = brightnessRGB.r + brightnessRGB.g + brightnessRGB.b; + var brightness = getBrightness(uv); // Dither: subtract a random value from the brightness brightness -= randomFloat( uv + vec2(time.seconds) ) * config.ditherMagnitude; - var paletteIndex = clamp(i32(brightness * 512.0), 0, 511); - // Map the brightness to a position in the palette texture - textureStore(outputTex, coord, vec4(palette.colors[paletteIndex] + config.backgroundColor, 1.0)); + var paletteIndex = clamp(i32(brightness.r * 512.0), 0, 511); + + textureStore(outputTex, coord, vec4( + palette.colors[paletteIndex] + + min(config.cursorColor * brightness.g, vec3(1.0)) + + config.backgroundColor, + 1.0 + )); } diff --git a/shaders/wgsl/rainPass.wgsl b/shaders/wgsl/rainPass.wgsl index 36ca8ac..5165dde 100644 --- a/shaders/wgsl/rainPass.wgsl +++ b/shaders/wgsl/rainPass.wgsl @@ -14,23 +14,21 @@ struct Config { brightnessThreshold : f32, brightnessOverride : f32, brightnessDecay : f32, - baseBrightness : f32, - baseContrast : f32, cursorBrightness : f32, cycleSpeed : f32, cycleFrameSkip : i32, fallSpeed : f32, - hasSun : i32, hasThunder : i32, raindropLength : f32, rippleScale : f32, rippleSpeed : f32, rippleThickness : f32, - cycleStyle : i32, rippleType : i32, // render-specific properties forwardSpeed : f32, + baseBrightness : f32, + baseContrast : f32, glyphVerticalSpacing : f32, glyphEdgeCrop : f32, isPolar : i32, @@ -38,6 +36,7 @@ struct Config { slantScale : f32, slantVec : vec2, volumetric : i32, + isolateCursor : i32, loops : i32, highPassThreshold : f32, }; @@ -58,6 +57,7 @@ struct Scene { struct Cell { raindrop : vec4, symbol : vec4, + effect : vec4, }; // The array of cells that the compute shader updates, and the fragment shader draws. @@ -128,9 +128,10 @@ fn wobble(x : f32) -> f32 { // Compute shader core functions -// Rain time is the shader's key underlying concept. +// This is the code rain's key underlying concept. // It's why glyphs that share a column are lit simultaneously, and are brighter toward the bottom. -fn getRainTime(simTime : f32, glyphPos : vec2) -> f32 { +// It's also why those bright areas are truncated into raindrops. +fn getRainBrightness(simTime : f32, glyphPos : vec2) -> f32 { var columnTimeOffset = randomFloat(vec2(glyphPos.x, 0.0)) * 1000.0; var columnSpeedOffset = randomFloat(vec2(glyphPos.x + 0.1, 0.0)) * 0.5 + 0.5; if (bool(config.loops)) { @@ -141,31 +142,16 @@ fn getRainTime(simTime : f32, glyphPos : vec2) -> f32 { if (!bool(config.loops)) { rainTime = wobble(rainTime); } - return rainTime; -} - -fn getBrightness(rainTime : f32) -> f32 { - return (1.0 - fract(rainTime)) * config.baseContrast + config.baseBrightness; -} - -fn getCycleSpeed(brightness : f32) -> f32 { - var localCycleSpeed = 1.0; - if (config.cycleStyle == 0 && brightness > 0.0) { - localCycleSpeed = pow(1.0 - brightness, 4.0); - } - return config.animationSpeed * config.cycleSpeed * localCycleSpeed; + return fract(rainTime); } // Compute shader additional effects -fn applySunShowerBrightness(brightness : f32, screenPos : vec2) -> f32 { - if (brightness >= -4.0) { - return pow(fract(brightness * 0.5), 3.0) * screenPos.y * 1.5; +fn getThunder(simTime : f32, screenPos : vec2) -> f32 { + if (!bool(config.hasThunder)) { + return 0.0; } - return brightness; -} -fn applyThunderBrightness(brightness : f32, simTime : f32, screenPos : vec2) -> f32 { var thunderTime = simTime * 0.5; var thunder = 1.0 - fract(wobble(thunderTime)); if (bool(config.loops)) { @@ -173,14 +159,13 @@ fn applyThunderBrightness(brightness : f32, simTime : f32, screenPos : vec2 } thunder = log(thunder * 1.5) * 4.0; - thunder = clamp(thunder, 0.0, 1.0); - thunder *= pow(screenPos.y, 2.0) * 3.0; - return brightness + thunder; + thunder = clamp(thunder, 0.0, 1.0) * 10.0 * pow(screenPos.y, 2.0); + return thunder; } -fn applyRippleEffect(effect : f32, simTime : f32, screenPos : vec2) -> f32 { +fn getRipple(simTime : f32, screenPos : vec2) -> f32 { if (config.rippleType == -1) { - return effect; + return 0.0; } var rippleTime = (simTime * 0.5 + sin(simTime) * 0.2) * config.rippleSpeed + 1.0; // TODO: clarify @@ -204,37 +189,19 @@ fn applyRippleEffect(effect : f32, simTime : f32, screenPos : vec2) -> f32 var rippleValue = fract(rippleTime) * config.rippleScale - rippleDistance; if (rippleValue > 0.0 && rippleValue < config.rippleThickness) { - return effect + 0.75; + return 0.75; } - return effect; + return 0.0; } // Compute shader main functions -fn computeShine (simTime : f32, isFirstFrame : bool, glyphPos : vec2, screenPos : vec2, previous : vec4) -> vec4 { - - // Determine the glyph's local time. - var rainTime = getRainTime(simTime, glyphPos); - var rainTimeBelow = getRainTime(simTime, glyphPos + vec2(0., -1.)); - var cursor = select(0.0, 1.0, fract(rainTime) < fract(rainTimeBelow)); - - // Rain time is the backbone of this effect. - - // Determine the glyph's brightness. - var brightness = getBrightness(rainTime); - - if (bool(config.hasSun)) { - brightness = applySunShowerBrightness(brightness, screenPos); - } - if (bool(config.hasThunder)) { - brightness = applyThunderBrightness(brightness, simTime, screenPos); - } - - // Determine the glyph's effect— the amount the glyph lights up for other reasons - var effect = 0.0; - effect = applyRippleEffect(effect, simTime, screenPos); // Round or square ripples across the grid +fn computeRaindrop (simTime : f32, isFirstFrame : bool, glyphPos : vec2, screenPos : vec2, previous : vec4) -> vec4 { + var brightness = getRainBrightness(simTime, glyphPos); + var brightnessBelow = getRainBrightness(simTime, glyphPos + vec2(0., -1.)); + var cursor = select(0.0, 1.0, brightness < brightnessBelow); // Blend the glyph's brightness with its previous brightness, so it winks on and off organically if (!isFirstFrame) { @@ -242,35 +209,31 @@ fn computeShine (simTime : f32, isFirstFrame : bool, glyphPos : vec2, scree brightness = mix(previousBrightness, brightness, config.brightnessDecay); } - var result = vec4(brightness, fract(rainTime), cursor, effect); + var result = vec4(brightness, cursor, 0.0, 0.0); return result; } fn computeSymbol (simTime : f32, isFirstFrame : bool, glyphPos : vec2, screenPos : vec2, previous : vec4, raindrop : vec4) -> vec4 { - var brightness = raindrop.r; - var previousSymbol = previous.r; var previousAge = previous.g; var resetGlyph = isFirstFrame; if (bool(config.loops)) { - resetGlyph = resetGlyph || brightness < 0.0; + resetGlyph = resetGlyph || raindrop.r < 0.0; } if (resetGlyph) { previousAge = randomFloat(screenPos + 0.5); previousSymbol = floor(config.glyphSequenceLength * randomFloat(screenPos)); } - var cycleSpeed = getCycleSpeed(brightness); + var cycleSpeed = config.animationSpeed * config.cycleSpeed; var age = previousAge; var symbol = previousSymbol; if (time.frames % config.cycleFrameSkip == 0) { age += cycleSpeed * f32(config.cycleFrameSkip); var advance = floor(age); - age = fract(age); - if (config.cycleStyle == 0) { - symbol = (symbol + advance) % config.glyphSequenceLength; - } else if (config.cycleStyle == 1 && advance > 0.) { + if (age > 1.0) { symbol = floor(config.glyphSequenceLength * randomFloat(screenPos + simTime)); + age = fract(age); } } @@ -278,6 +241,15 @@ fn computeSymbol (simTime : f32, isFirstFrame : bool, glyphPos : vec2, scre return result; } +fn computeEffect (simTime : f32, isFirstFrame : bool, glyphPos : vec2, screenPos : vec2, previous : vec4, raindrop : vec4) -> vec4 { + + var multipliedEffects = 1.0 + getThunder(simTime, screenPos); + var addedEffects = getRipple(simTime, screenPos); // Round or square ripples across the grid + + var result = vec4(multipliedEffects, addedEffects, 0.0, 0.0); + return result; +} + @compute @workgroup_size(32, 1, 1) fn computeMain(input : ComputeInput) { // Resolve the invocation ID to a cell coordinate @@ -298,8 +270,9 @@ fn computeSymbol (simTime : f32, isFirstFrame : bool, glyphPos : vec2, scre var screenPos = glyphPos / config.gridSize; var cell = cells_RW.cells[i]; - cell.raindrop = computeShine(simTime, isFirstFrame, glyphPos, screenPos, cell.raindrop); + cell.raindrop = computeRaindrop(simTime, isFirstFrame, glyphPos, screenPos, cell.raindrop); cell.symbol = computeSymbol(simTime, isFirstFrame, glyphPos, screenPos, cell.symbol, cell.raindrop); + cell.effect = computeEffect(simTime, isFirstFrame, glyphPos, screenPos, cell.effect, cell.raindrop); cells_RW.cells[i] = cell; } @@ -367,89 +340,117 @@ fn median3(i : vec3) -> f32 { return max(min(i.r, i.g), min(max(i.r, i.g), i.b)); } +fn getUV(inputUV : vec2) -> vec2 { + + var uv = inputUV; + + if (bool(config.volumetric)) { + return uv; + } + + if (bool(config.isPolar)) { + // Curved space to make the letters appear to radiate from up above + uv -= 0.5; + uv *= 0.5; + uv.y -= 0.5; + var radius = length(uv); + var angle = atan2(uv.y, uv.x) / (2.0 * PI) + 0.5; + uv = vec2(fract(angle * 4.0 - 0.5), 1.5 * (1.0 - sqrt(radius))); + } else { + // Apply the slant and a scale to space so the viewport is still fully covered by the geometry + uv = vec2( + (uv.x - 0.5) * config.slantVec.x + (uv.y - 0.5) * config.slantVec.y, + (uv.y - 0.5) * config.slantVec.x - (uv.x - 0.5) * config.slantVec.y + ) * config.slantScale + 0.5; + } + + uv.y /= config.glyphHeightToWidth; + + return uv; +} + +fn getBrightness(inputBrightness : f32, cursor : f32, quadDepth : f32, multipliedEffects : f32, addedEffects : f32) -> vec2 { + + var isCursor = bool(cursor); + + if (!bool(config.isolateCursor)) { + isCursor = false; + } + + var brightness = (1.0 - inputBrightness) * config.baseContrast + config.baseBrightness; + + // Modes that don't fade glyphs set their actual brightness here + if (config.brightnessOverride > 0. && brightness > config.brightnessThreshold && !isCursor) { + brightness = config.brightnessOverride; + } + + brightness *= multipliedEffects; + brightness += addedEffects; + + // In volumetric mode, distant glyphs are dimmer + if (bool(config.volumetric) && !bool(config.showDebugView)) { + brightness = brightness * min(1.0, quadDepth); + } + + return select(vec2(1.0, 0.0), vec2(0.0, 1.0), isCursor) * brightness; +} + fn getSymbolUV(symbol : i32) -> vec2 { var symbolX = symbol % config.glyphTextureGridSize.x; var symbolY = symbol / config.glyphTextureGridSize.x; return vec2(f32(symbolX), f32(symbolY)); } +fn getSymbol(cellUV : vec2, index : i32) -> f32 { + // resolve UV to cropped position of glyph in MSDF texture + var uv = fract(cellUV * config.gridSize); + uv.y = 1.0 - uv.y; // WebGL -> WebGPU y-flip + uv -= 0.5; + uv *= clamp(1.0 - config.glyphEdgeCrop, 0.0, 1.0); + uv += 0.5; + uv = (uv + getSymbolUV(index)) / vec2(config.glyphTextureGridSize); + + // MSDF: calculate brightness of fragment based on distance to shape + var dist = textureSample(msdfTexture, linearSampler, uv).rgb; + var sigDist = median3(dist) - 0.5; + return clamp(sigDist / fwidth(sigDist) + 0.5, 0.0, 1.0); +} + // Fragment shader @fragment fn fragMain(input : VertOutput) -> FragOutput { - var volumetric = bool(config.volumetric); - var uv = input.uv; - - // For normal mode, derive the fragment's glyph and msdf UV from its screen space position - if (!volumetric) { - if (bool(config.isPolar)) { - // Curve space to make the letters appear to radiate from up above - uv -= 0.5; - uv *= 0.5; - uv.y -= 0.5; - var radius = length(uv); - var angle = atan2(uv.y, uv.x) / (2.0 * PI) + 0.5; - uv = vec2(fract(angle * 4.0 - 0.5), 1.5 * (1.0 - sqrt(radius))); - - } else { - // Apply the slant and a scale to space so the viewport is still fully covered by the geometry - uv = vec2( - (uv.x - 0.5) * config.slantVec.x + (uv.y - 0.5) * config.slantVec.y, - (uv.y - 0.5) * config.slantVec.x - (uv.x - 0.5) * config.slantVec.y - ) * config.slantScale + 0.5; - } - uv.y /= config.glyphHeightToWidth; - } + var uv = getUV(input.uv); // Retrieve cell var gridCoord : vec2 = vec2(uv * config.gridSize); var gridIndex = gridCoord.y * i32(config.gridSize.x) + gridCoord.x; var cell = cells_RO.cells[gridIndex]; - var symbolUV = getSymbolUV(i32(cell.symbol.r)); - var brightness = cell.raindrop.r; - - // Modes that don't fade glyphs set their actual brightness here - if (config.brightnessOverride > 0.0 && brightness > config.brightnessThreshold) { - brightness = config.brightnessOverride; - } - - brightness = max(cell.raindrop.b * config.cursorBrightness, brightness); - brightness = max(cell.raindrop.a, brightness); - - // In volumetric mode, distant glyphs are dimmer - if (volumetric) { - brightness *= min(1.0, input.quadDepth); - } - - // resolve UV to cropped position of glyph in MSDF texture - var glyphUV = fract(uv * config.gridSize); - glyphUV.y = 1.0 - glyphUV.y; // WebGL -> WebGPU y-flip - glyphUV -= 0.5; - glyphUV *= clamp(1.0 - config.glyphEdgeCrop, 0.0, 1.0); - glyphUV += 0.5; - var msdfUV = (glyphUV + symbolUV) / vec2(config.glyphTextureGridSize); - - // MSDF : calculate brightness of fragment based on distance to shape - var dist = textureSample(msdfTexture, linearSampler, msdfUV).rgb; - var sigDist = median3(dist) - 0.5; - var alpha = clamp(sigDist / fwidth(sigDist) + 0.5, 0.0, 1.0); + var brightness = getBrightness( + cell.raindrop.r, + cell.raindrop.g, + input.quadDepth, + cell.effect.r, + cell.effect.g + ); + var symbol = getSymbol(uv, i32(cell.symbol.r)); var output : FragOutput; if (bool(config.showDebugView)) { output.color = vec4( vec3( - cell.raindrop.b, + cell.raindrop.g, vec2( - 1.0 - (cell.raindrop.g * 3.0), - 1.0 - (cell.raindrop.g * 10.0) - ) * (1.0 - cell.raindrop.b) - ) * alpha, - 1. + 1.0 - (cell.raindrop.r * 3.0), + 1.0 - (cell.raindrop.r * 8.0) + ) * (1.0 - cell.raindrop.g) + ) * symbol, + 1.0 ); } else { - output.color = vec4(brightness * alpha, 0., 0., 1.0); + output.color = vec4(brightness * symbol, 0.0, 0.0); } var highPassColor = output.color; diff --git a/shaders/wgsl/stripePass.wgsl b/shaders/wgsl/stripePass.wgsl index f6580dd..d74f1f8 100644 --- a/shaders/wgsl/stripePass.wgsl +++ b/shaders/wgsl/stripePass.wgsl @@ -2,6 +2,7 @@ struct Config { bloomStrength : f32, ditherMagnitude : f32, backgroundColor : vec3, + cursorColor : vec3 }; struct Time { @@ -52,10 +53,15 @@ fn getBrightness(uv : vec2) -> vec4 { var color = textureSampleLevel( stripeTexture, linearSampler, uv, 0.0 ).rgb; - var brightness = getBrightness(uv).r; + var brightness = getBrightness(uv); // Dither: subtract a random value from the brightness brightness -= randomFloat( uv + vec2(time.seconds) ) * config.ditherMagnitude; - textureStore(outputTex, coord, vec4(color * brightness + config.backgroundColor, 1.0)); + textureStore(outputTex, coord, vec4( + color * brightness.r + + min(config.cursorColor * brightness.g, vec3(1.0)) + + config.backgroundColor, + 1.0 + )); }