diff --git a/TODO.txt b/TODO.txt index a2fd2d6..6c068ce 100644 --- a/TODO.txt +++ b/TODO.txt @@ -17,14 +17,11 @@ Playdate version Maybe crank sounds? Not sure yet Support Resurrections anomaly streaks - Kind of like the cursor: new rain pass output channel, new config prop - MSDF - They should line up in Photoshop without too much trouble, actually - Texture - Maybe give normal Matrix code a pixel grill texture + grit texture multiply Lighting Different parts of a streak glow at different intensities, at different times The streaks often dim slower, ie. are brighter, than the glyphs beneath them + Different brightness/contrast? Imagine they're metallic or something Support Resurrections SDF bevel and "lights" diff --git a/assets/resurrections_glint_msdf.png b/assets/resurrections_glint_msdf.png new file mode 100644 index 0000000..ff10297 Binary files /dev/null and b/assets/resurrections_glint_msdf.png differ diff --git a/js/config.js b/js/config.js index 8bebc52..f009965 100644 --- a/js/config.js +++ b/js/config.js @@ -26,6 +26,7 @@ const fonts = { resurrections: { // The glyphs seen in the film trilogy glyphTexURL: "assets/resurrections_msdf.png", + glintTexURL: "assets/resurrections_glint_msdf.png", glyphSequenceLength: 135, glyphTextureGridSize: [13, 12], }, @@ -57,6 +58,8 @@ const defaults = { backgroundColor: [0, 0, 0], // The color "behind" the glyphs isolateCursor: true, // Whether the "cursor"— the brightest glyph at the bottom of a raindrop— has its own color cursorColor: [1.5, 2, 0.9], // The color of the cursor + isolateGlint: false, // Whether the "glint"— highlights on certain symbols in the font— should appear + glintColor: [1, 1, 1], // The color of the glint volumetric: false, // A mode where the raindrops appear in perspective animationSpeed: 1, // The global rate that all animations progress forwardSpeed: 0.25, // The speed volumetric rain approaches the eye @@ -188,6 +191,28 @@ const versions = { { hsl: [0.375, 1.0, 1.0], at: 1.0 }, ], }, + trinity: { + font: "resurrections", + glyphEdgeCrop: 0.1, + cursorColor: [1.4, 2, 1.2], + isolateGlint: true, + glintColor: [2, 1.5, 0.5], + baseBrightness: -0.9, + baseContrast: 1.5, + highPassThreshold: 0, + numColumns: 50, + cycleSpeed: 0.03, + bloomStrength: 0.7, + fallSpeed: 0.3, + paletteEntries: [ + { hsl: [0.4, 0.9, 0.0], at: 0.0 }, + { hsl: [0.4, 1.0, 0.5], at: 1.0 }, + ], + volumetric: true, + forwardSpeed: 0.2, + raindropLength: 0.3, + density: 0.5, + }, palimpsest: { font: "huberfishA", isolateCursor: false, @@ -303,6 +328,7 @@ const paramMapping = { stripeColors: { key: "stripeColors", parser: (s) => s }, backgroundColor: { key: "backgroundColor", parser: (s) => s.split(",").map(parseFloat) }, cursorColor: { key: "cursorColor", parser: (s) => s.split(",").map(parseFloat) }, + glintColor: { key: "glintColor", parser: (s) => s.split(",").map(parseFloat) }, volumetric: { key: "volumetric", parser: (s) => s.toLowerCase().includes("true") }, loops: { key: "loops", parser: (s) => s.toLowerCase().includes("true") }, renderer: { key: "renderer", parser: (s) => s }, @@ -321,8 +347,14 @@ export default (urlParams) => { .filter(([_, value]) => value != null) ); - if (validParams.effect != null && validParams.cursorColor == null) { - validParams.cursorColor = [2, 2, 2]; + if (validParams.effect != null) { + if (validParams.cursorColor == null) { + validParams.cursorColor = [2, 2, 2]; + } + + if (validParams.glintColor == null) { + validParams.glintColor = [1, 1, 1]; + } } const version = validParams.version in versions ? versions[validParams.version] : versions.classic; diff --git a/js/regl/palettePass.js b/js/regl/palettePass.js index 065ee6e..83cbd71 100644 --- a/js/regl/palettePass.js +++ b/js/regl/palettePass.js @@ -65,7 +65,7 @@ const makePalette = (regl, entries) => { export default ({ regl, config }, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); const palette = makePalette(regl, config.paletteEntries); - const { backgroundColor, cursorColor, ditherMagnitude, bloomStrength } = config; + const { backgroundColor, cursorColor, glintColor, ditherMagnitude, bloomStrength } = config; const palettePassFrag = loadText("shaders/glsl/palettePass.frag.glsl"); @@ -75,6 +75,7 @@ export default ({ regl, config }, inputs) => { uniforms: { backgroundColor, cursorColor, + glintColor, ditherMagnitude, bloomStrength, tex: inputs.primary, diff --git a/js/regl/rainPass.js b/js/regl/rainPass.js index 31e4f6d..464ed2f 100644 --- a/js/regl/rainPass.js +++ b/js/regl/rainPass.js @@ -116,6 +116,7 @@ export default ({ regl, config, lkg }) => { // We render the code into an FBO using MSDFs: https://github.com/Chlumsky/msdfgen const msdf = loadImage(regl, config.glyphTexURL); + const glintMSDF = loadImage(regl, config.glintTexURL); const rainPassVert = loadText("shaders/glsl/rainPass.vert.glsl"); const rainPassFrag = loadText("shaders/glsl/rainPass.frag.glsl"); const output = makePassFBO(regl, config.useHalfFloat); @@ -131,6 +132,7 @@ export default ({ regl, config, lkg }) => { "brightnessThreshold", "brightnessOverride", "isolateCursor", + "isolateGlint", "glyphEdgeCrop", "isPolar", ]), @@ -160,6 +162,7 @@ export default ({ regl, config, lkg }) => { symbolState: symbolDoubleBuffer.front, effectState: effectDoubleBuffer.front, glyphTex: msdf.texture, + glintTex: glintMSDF.texture, camera: regl.prop("camera"), transform: regl.prop("transform"), @@ -201,7 +204,7 @@ export default ({ regl, config, lkg }) => { { primary: output, }, - Promise.all([msdf.loaded, rainPassShine.loaded, rainPassSymbol.loaded, rainPassVert.loaded, rainPassFrag.loaded]), + Promise.all([msdf.loaded, glintMSDF.loaded, rainPassShine.loaded, rainPassSymbol.loaded, rainPassVert.loaded, rainPassFrag.loaded]), (w, h) => { output.resize(w, h); const aspectRatio = w / h; diff --git a/js/regl/stripePass.js b/js/regl/stripePass.js index 903a1ef..d82e032 100644 --- a/js/regl/stripePass.js +++ b/js/regl/stripePass.js @@ -31,7 +31,7 @@ const prideStripeColors = [ export default ({ regl, config }, inputs) => { const output = makePassFBO(regl, config.useHalfFloat); - const { backgroundColor, cursorColor, ditherMagnitude, bloomStrength } = config; + const { backgroundColor, cursorColor, glintColor, ditherMagnitude, bloomStrength } = config; // Expand and convert stripe colors into 1D texture data const stripeColors = @@ -50,6 +50,7 @@ export default ({ regl, config }, inputs) => { uniforms: { backgroundColor, cursorColor, + glintColor, ditherMagnitude, bloomStrength, tex: inputs.primary, diff --git a/js/regl/utils.js b/js/regl/utils.js index 885abea..a56e31b 100644 --- a/js/regl/utils.js +++ b/js/regl/utils.js @@ -30,7 +30,7 @@ const loadImage = (regl, url) => { let loaded = false; return { texture: () => { - if (!loaded) { + if (!loaded && url != null) { console.warn(`texture still loading: ${url}`); } return texture; diff --git a/js/webgpu/palettePass.js b/js/webgpu/palettePass.js index 126c1b6..960c0fa 100644 --- a/js/webgpu/palettePass.js +++ b/js/webgpu/palettePass.js @@ -108,6 +108,7 @@ export default ({ config, device, timeBuffer }) => { ditherMagnitude: config.ditherMagnitude, backgroundColor: config.backgroundColor, cursorColor: config.cursorColor, + glintColor: config.glintColor, }); const paletteUniforms = paletteShaderUniforms.Palette; diff --git a/js/webgpu/rainPass.js b/js/webgpu/rainPass.js index d43023e..75b95f3 100644 --- a/js/webgpu/rainPass.js +++ b/js/webgpu/rainPass.js @@ -26,7 +26,7 @@ const makeConfigBuffer = (device, configUniforms, config, density, gridSize) => export default ({ config, device, timeBuffer }) => { const { mat4, vec3 } = glMatrix; - const assets = [loadTexture(device, config.glyphTexURL), loadShader(device, "shaders/wgsl/rainPass.wgsl")]; + const assets = [loadTexture(device, config.glyphTexURL), loadTexture(device, config.glintTexURL), loadShader(device, "shaders/wgsl/rainPass.wgsl")]; // The volumetric mode multiplies the number of columns // to reach the desired density, and then overlaps them @@ -85,7 +85,7 @@ export default ({ config, device, timeBuffer }) => { let highPassOutput; const loaded = (async () => { - const [msdfTexture, rainShader] = await Promise.all(assets); + const [msdfTexture, glintMSDFTexture, rainShader] = await Promise.all(assets); const rainShaderUniforms = structs.from(rainShader.code); configBuffer = makeConfigBuffer(device, rainShaderUniforms.Config, config, density, gridSize); @@ -143,7 +143,15 @@ export default ({ config, device, timeBuffer }) => { ]); computeBindGroup = makeBindGroup(device, computePipeline, 0, [configBuffer, timeBuffer, cellsBuffer]); - renderBindGroup = makeBindGroup(device, renderPipeline, 0, [configBuffer, timeBuffer, sceneBuffer, linearSampler, msdfTexture.createView(), cellsBuffer]); + renderBindGroup = makeBindGroup(device, renderPipeline, 0, [ + configBuffer, + timeBuffer, + sceneBuffer, + linearSampler, + msdfTexture.createView(), + glintMSDFTexture.createView(), + cellsBuffer, + ]); })(); const build = (size) => { diff --git a/js/webgpu/stripePass.js b/js/webgpu/stripePass.js index 64467d4..667422a 100644 --- a/js/webgpu/stripePass.js +++ b/js/webgpu/stripePass.js @@ -79,6 +79,7 @@ export default ({ config, device, timeBuffer }) => { ditherMagnitude: config.ditherMagnitude, backgroundColor: config.backgroundColor, cursorColor: config.cursorColor, + glintColor: config.glintColor, }); })(); diff --git a/js/webgpu/utils.js b/js/webgpu/utils.js index ecee5d3..608fc24 100644 --- a/js/webgpu/utils.js +++ b/js/webgpu/utils.js @@ -20,6 +20,14 @@ const loadTexture = async (device, url) => { */ const loadTexture = async (device, url) => { + if (url == null) { + return device.createTexture({ + size: [1, 1, 1], + format: "rgba8unorm", + usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT, + }); + } + const image = new Image(); image.crossOrigin = "Anonymous"; image.src = url; diff --git a/shaders/glsl/palettePass.frag.glsl b/shaders/glsl/palettePass.frag.glsl index b2d5322..753f856 100644 --- a/shaders/glsl/palettePass.frag.glsl +++ b/shaders/glsl/palettePass.frag.glsl @@ -7,7 +7,7 @@ uniform sampler2D palette; uniform float bloomStrength; uniform float ditherMagnitude; uniform float time; -uniform vec3 backgroundColor, cursorColor; +uniform vec3 backgroundColor, cursorColor, glintColor; varying vec2 vUV; highp float rand( const in vec2 uv, const in float t ) { @@ -26,12 +26,13 @@ void main() { vec4 brightness = getBrightness(vUV); // Dither: subtract a random value from the brightness - brightness -= rand( gl_FragCoord.xy, time ) * ditherMagnitude; + brightness -= rand( gl_FragCoord.xy, time ) * ditherMagnitude / 3.0; // 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, vec3(1.0)) + + min(glintColor * brightness.b, vec3(1.0)) + backgroundColor, 1.0 ); diff --git a/shaders/glsl/rainPass.frag.glsl b/shaders/glsl/rainPass.frag.glsl index 0b8e138..2ec4937 100644 --- a/shaders/glsl/rainPass.frag.glsl +++ b/shaders/glsl/rainPass.frag.glsl @@ -6,7 +6,7 @@ precision lowp float; uniform sampler2D raindropState, symbolState, effectState; uniform float numColumns, numRows; -uniform sampler2D glyphTex; +uniform sampler2D glyphTex, glintTex; uniform float glyphHeightToWidth, glyphSequenceLength, glyphEdgeCrop; uniform float baseContrast, baseBrightness; uniform float brightnessOverride, brightnessThreshold; @@ -16,7 +16,7 @@ uniform float slantScale; uniform bool isPolar; uniform bool showDebugView; uniform bool volumetric; -uniform bool isolateCursor; +uniform bool isolateCursor, isolateGlint; varying vec2 vUV; varying vec4 vRaindrop, vSymbol, vEffect; @@ -87,7 +87,7 @@ vec2 getSymbolUV(float index) { return vec2(symbolX, symbolY); } -float getSymbol(vec2 uv, float index) { +vec2 getSymbol(vec2 uv, float index) { // resolve UV to cropped position of glyph in MSDF texture uv = fract(uv * vec2(numColumns, numRows)); uv -= 0.5; @@ -96,9 +96,20 @@ float getSymbol(vec2 uv, float index) { uv = (uv + getSymbolUV(index)) / glyphTextureGridSize; // 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.); + vec2 symbol; + { + vec3 dist = texture2D(glyphTex, uv).rgb; + float sigDist = median3(dist) - 0.5; + symbol.r = clamp(sigDist / fwidth(sigDist) + 0.5, 0., 1.); + } + + if (isolateGlint) { + vec3 dist = texture2D(glintTex, uv).rgb; + float sigDist = median3(dist) - 0.5; + symbol.g = clamp(sigDist / fwidth(sigDist) + 0.5, 0., 1.); + } + + return symbol; } void main() { @@ -111,7 +122,7 @@ void main() { vec4 effectData = volumetric ? vEffect : texture2D( effectState, uv); vec2 brightness = getBrightness(raindropData.r, raindropData.g, effectData.r, effectData.g); - float symbol = getSymbol(uv, symbolData.r); + vec2 symbol = getSymbol(uv, symbolData.r); if (showDebugView) { gl_FragColor = vec4( @@ -121,10 +132,10 @@ void main() { 1. - (raindropData.r * 3.), 1. - (raindropData.r * 8.) ) * (1. - raindropData.g) - ) * symbol, + ) * symbol.r, 1. ); } else { - gl_FragColor = vec4(brightness * symbol, 0., 0.); + gl_FragColor = vec4(brightness * symbol.r, brightness.r * symbol.g, 0.); } } diff --git a/shaders/glsl/stripePass.frag.glsl b/shaders/glsl/stripePass.frag.glsl index 18ea0ac..425a0a4 100644 --- a/shaders/glsl/stripePass.frag.glsl +++ b/shaders/glsl/stripePass.frag.glsl @@ -7,7 +7,7 @@ uniform float bloomStrength; uniform sampler2D stripes; uniform float ditherMagnitude; uniform float time; -uniform vec3 backgroundColor, cursorColor; +uniform vec3 backgroundColor, cursorColor, glintColor; varying vec2 vUV; highp float rand( const in vec2 uv, const in float t ) { @@ -28,11 +28,12 @@ void main() { vec4 brightness = getBrightness(vUV); // Dither: subtract a random value from the brightness - brightness -= rand( gl_FragCoord.xy, time ) * ditherMagnitude; + brightness -= rand( gl_FragCoord.xy, time ) * ditherMagnitude / 3.0; gl_FragColor = vec4( color * brightness.r - + min(cursorColor * brightness.g, 1.0) + + min(cursorColor * brightness.g, vec3(1.0)) + + min(glintColor * brightness.b, vec3(1.0)) + backgroundColor, 1.0 ); diff --git a/shaders/wgsl/palettePass.wgsl b/shaders/wgsl/palettePass.wgsl index 06f01ee..d46365e 100644 --- a/shaders/wgsl/palettePass.wgsl +++ b/shaders/wgsl/palettePass.wgsl @@ -2,7 +2,8 @@ struct Config { bloomStrength : f32, ditherMagnitude : f32, backgroundColor : vec3, - cursorColor : vec3 + cursorColor : vec3, + glintColor : vec3, }; struct Palette { @@ -58,7 +59,7 @@ fn getBrightness(uv : vec2) -> vec4 { var brightness = getBrightness(uv); // Dither: subtract a random value from the brightness - brightness -= randomFloat( uv + vec2(time.seconds) ) * config.ditherMagnitude; + brightness -= randomFloat( uv + vec2(time.seconds) ) * config.ditherMagnitude / 3.0; // Map the brightness to a position in the palette texture var paletteIndex = clamp(i32(brightness.r * 512.0), 0, 511); @@ -66,6 +67,7 @@ fn getBrightness(uv : vec2) -> vec4 { textureStore(outputTex, coord, vec4( palette.colors[paletteIndex] + min(config.cursorColor * brightness.g, vec3(1.0)) + + min(config.glintColor * brightness.b, vec3(1.0)) + config.backgroundColor, 1.0 )); diff --git a/shaders/wgsl/rainPass.wgsl b/shaders/wgsl/rainPass.wgsl index 5165dde..5309a3b 100644 --- a/shaders/wgsl/rainPass.wgsl +++ b/shaders/wgsl/rainPass.wgsl @@ -37,6 +37,7 @@ struct Config { slantVec : vec2, volumetric : i32, isolateCursor : i32, + isolateGlint : i32, loops : i32, highPassThreshold : f32, }; @@ -76,7 +77,8 @@ struct CellData { @group(0) @binding(2) var scene : Scene; @group(0) @binding(3) var linearSampler : sampler; @group(0) @binding(4) var msdfTexture : texture_2d; -@group(0) @binding(5) var cells_RO : CellData; +@group(0) @binding(5) var glintMSDFTexture : texture_2d; +@group(0) @binding(6) var cells_RO : CellData; // Shader params @@ -401,7 +403,7 @@ fn getSymbolUV(symbol : i32) -> vec2 { return vec2(f32(symbolX), f32(symbolY)); } -fn getSymbol(cellUV : vec2, index : i32) -> f32 { +fn getSymbol(cellUV : vec2, index : i32) -> vec2 { // 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 @@ -410,10 +412,22 @@ fn getSymbol(cellUV : vec2, index : i32) -> f32 { uv += 0.5; uv = (uv + getSymbolUV(index)) / vec2(config.glyphTextureGridSize); + var symbol = vec2(); + // 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); + { + var dist = textureSample(msdfTexture, linearSampler, uv).rgb; + var sigDist = median3(dist) - 0.5; + symbol.r = clamp(sigDist / fwidth(sigDist) + 0.5, 0.0, 1.0); + } + + if (bool(config.isolateGlint)) { + var dist = textureSample(glintMSDFTexture, linearSampler, uv).rgb; + var sigDist = median3(dist) - 0.5; + symbol.g = clamp(sigDist / fwidth(sigDist) + 0.5, 0.0, 1.0); + } + + return symbol; } // Fragment shader @@ -446,11 +460,11 @@ fn getSymbol(cellUV : vec2, index : i32) -> f32 { 1.0 - (cell.raindrop.r * 3.0), 1.0 - (cell.raindrop.r * 8.0) ) * (1.0 - cell.raindrop.g) - ) * symbol, + ) * symbol.r, 1.0 ); } else { - output.color = vec4(brightness * symbol, 0.0, 0.0); + output.color = vec4(brightness * symbol.r, brightness.r * symbol.g, 0.0); } var highPassColor = output.color; diff --git a/shaders/wgsl/stripePass.wgsl b/shaders/wgsl/stripePass.wgsl index d74f1f8..3b4b18a 100644 --- a/shaders/wgsl/stripePass.wgsl +++ b/shaders/wgsl/stripePass.wgsl @@ -2,7 +2,8 @@ struct Config { bloomStrength : f32, ditherMagnitude : f32, backgroundColor : vec3, - cursorColor : vec3 + cursorColor : vec3, + glintColor : vec3, }; struct Time { @@ -56,11 +57,12 @@ fn getBrightness(uv : vec2) -> vec4 { var brightness = getBrightness(uv); // Dither: subtract a random value from the brightness - brightness -= randomFloat( uv + vec2(time.seconds) ) * config.ditherMagnitude; + brightness -= randomFloat( uv + vec2(time.seconds) ) * config.ditherMagnitude / 3.0; textureStore(outputTex, coord, vec4( color * brightness.r + min(config.cursorColor * brightness.g, vec3(1.0)) + + min(config.glintColor * brightness.b, vec3(1.0)) + config.backgroundColor, 1.0 ));