diff --git a/docs/src/app/(shaders)/warp/page.tsx b/docs/src/app/(shaders)/warp/page.tsx index 6ec92ae40..eae0ee5e0 100644 --- a/docs/src/app/(shaders)/warp/page.tsx +++ b/docs/src/app/(shaders)/warp/page.tsx @@ -22,6 +22,8 @@ const WarpWithControls = () => { const [params, setParams] = useControls(() => { return { + contour: { value: defaults.contour, min: 0, max: 1, order: 10 }, + antialiasing: { value: defaults.antialiasing, order: 11 }, proportion: { value: defaults.proportion, min: 0, max: 1, order: 100 }, softness: { value: defaults.softness, min: 0, max: 1, order: 101 }, distortion: { value: defaults.distortion, min: 0, max: 1, order: 102 }, diff --git a/docs/src/shader-defs/warp-def.ts b/docs/src/shader-defs/warp-def.ts index e7742c6f3..33ac00ec5 100644 --- a/docs/src/shader-defs/warp-def.ts +++ b/docs/src/shader-defs/warp-def.ts @@ -71,6 +71,13 @@ export const warpDef: ShaderDef = { defaultValue: defaultParams.shapeScale, description: 'Zoom level of the base pattern', }, + { + name: 'antialiasing', + type: 'boolean', + defaultValue: defaultParams.antialiasing, + description: 'Fixed 4x4 supersampling; needed for crisp low-softness bands under strong distortion/swirl', + options: ['true', 'false'], + }, ...animatedCommonParams, ], }; diff --git a/packages/shaders-react/src/shaders/warp.tsx b/packages/shaders-react/src/shaders/warp.tsx index b2fe60180..01c0dd657 100644 --- a/packages/shaders-react/src/shaders/warp.tsx +++ b/packages/shaders-react/src/shaders/warp.tsx @@ -32,6 +32,8 @@ export const defaultPreset: WarpPreset = { swirlIterations: 10, shapeScale: 0.1, shape: 'checks', + contour: 0, + antialiasing: false }, }; @@ -51,6 +53,8 @@ export const presetCauldron: WarpPreset = { swirlIterations: 7, shapeScale: 0.6, shape: 'edge', + contour: 0, + antialiasing: false }, }; @@ -71,6 +75,8 @@ export const presetInk: WarpPreset = { swirlIterations: 10, shapeScale: 0.28, shape: 'checks', + contour: 0, + antialiasing: true }, }; @@ -90,6 +96,8 @@ export const presetKelp: WarpPreset = { swirlIterations: 3, shapeScale: 1, shape: 'stripes', + contour: 0, + antialiasing: false }, }; @@ -110,6 +118,8 @@ export const presetNectar: WarpPreset = { swirlIterations: 10, shapeScale: 0.75, shape: 'edge', + contour: 0, + antialiasing: false }, }; @@ -129,6 +139,8 @@ export const presetPassion: WarpPreset = { swirlIterations: 6, shapeScale: 0.25, shape: 'checks', + contour: 0, + antialiasing: false }, }; @@ -153,6 +165,8 @@ export const Warp: React.FC = memo(function WarpImpl({ swirlIterations = defaultPreset.params.swirlIterations, shapeScale = defaultPreset.params.shapeScale, shape = defaultPreset.params.shape, + contour = defaultPreset.params.contour, + antialiasing = defaultPreset.params.antialiasing, // Sizing props fit = defaultPreset.params.fit, @@ -177,6 +191,8 @@ export const Warp: React.FC = memo(function WarpImpl({ u_swirlIterations: swirlIterations, u_shapeScale: shapeScale, u_shape: WarpPatterns[shape], + u_contour: contour, + u_antialiasing: antialiasing, u_noiseTexture: getShaderNoiseTexture(), // Sizing uniforms diff --git a/packages/shaders/src/shaders/warp.ts b/packages/shaders/src/shaders/warp.ts index 95a0bfc94..e999ca301 100644 --- a/packages/shaders/src/shaders/warp.ts +++ b/packages/shaders/src/shaders/warp.ts @@ -24,6 +24,10 @@ export const warpMeta = { * - u_distortion (float): Strength of noise-based distortion (0 to 1) * - u_swirl (float): Strength of the swirl distortion (0 to 1) * - u_swirlIterations (float): Number of layered swirl passes, effective with swirl > 0 (0 to 20) + * - u_contour (float): Strength of the contour band traced around the canvas element borders (0 to 1) + * - u_antialiasing (bool): Enables fixed 4x4 supersampling; needed for crisp low-softness bands under strong distortion/swirl + * - u_resolution (vec2): Canvas resolution in pixels, used to keep the contour locked to the canvas borders + * - u_pixelRatio (float): Device pixel ratio, used to keep the contour thickness consistent across displays * - u_noiseTexture (sampler2D): Pre-computed randomizer source texture * * Vertex shader outputs (used in fragment shader): @@ -46,14 +50,14 @@ export const warpMeta = { // language=GLSL export const warpFragmentShader: string = `#version 300 es -precision mediump float; +precision lowp float; uniform float u_time; uniform float u_scale; uniform sampler2D u_noiseTexture; -uniform vec4 u_colors[${ warpMeta.maxColorCount }]; +uniform vec4 u_colors[${warpMeta.maxColorCount}]; uniform float u_colorsCount; uniform float u_proportion; uniform float u_softness; @@ -62,13 +66,17 @@ uniform float u_shapeScale; uniform float u_distortion; uniform float u_swirl; uniform float u_swirlIterations; +uniform float u_contour; +uniform bool u_antialiasing; +uniform mediump vec2 u_resolution; +uniform mediump float u_pixelRatio; in vec2 v_patternUV; out vec4 fragColor; -${ declarePI } -${ rotation2 } +${declarePI} +${rotation2} float randomG(vec2 p) { vec2 uv = floor(p) / 100. + .5; return texture(u_noiseTexture, fract(uv)).g; @@ -86,59 +94,43 @@ float valueNoise(vec2 st) { return mix(x1, x2, u.y); } - -void main() { - vec2 uv = v_patternUV; - uv *= .5; - - const float firstFrameOffset = 118.; - float t = 0.0625 * (u_time + firstFrameOffset); - - float n1 = valueNoise(uv * 1. + t); - float n2 = valueNoise(uv * 2. - t); - float angle = n1 * TWO_PI; - uv.x += 4. * u_distortion * n2 * cos(angle); - uv.y += 4. * u_distortion * n2 * sin(angle); - - float swirl = u_swirl; - for (int i = 1; i <= 20; i++) { - if (i >= int(u_swirlIterations)) break; - float iFloat = float(i); - // swirl *= (1. - smoothstep(.0, .25, length(fwidth(uv)))); - uv.x += swirl / iFloat * cos(t + iFloat * 1.5 * uv.y); - uv.y += swirl / iFloat * cos(t + iFloat * 1. * uv.x); - } - +// The scalar base pattern (checks / stripes / edge) evaluated at a warped UV. +float patternShape(vec2 uv) { float proportion = clamp(u_proportion, 0., 1.); - - float shape = 0.; + float proportionOffset = .48 * sign(proportion - .5) * pow(abs(proportion - .5), .5); + float shape; if (u_shape < .5) { - vec2 checksShape_uv = uv * (.5 + 3.5 * u_shapeScale); - shape = .5 + .5 * sin(checksShape_uv.x) * cos(checksShape_uv.y); - shape += .48 * sign(proportion - .5) * pow(abs(proportion - .5), .5); + vec2 p = uv * (.5 + 3.5 * u_shapeScale); + shape = .5 + .5 * sin(p.x) * cos(p.y); + shape += proportionOffset; } else if (u_shape < 1.5) { - vec2 stripesShape_uv = uv * (2. * u_shapeScale); - float f = fract(stripesShape_uv.y); + float f = fract(uv.y * (2. * u_shapeScale)); shape = smoothstep(.0, .55, f) * (1.0 - smoothstep(.45, 1., f)); - shape += .48 * sign(proportion - .5) * pow(abs(proportion - .5), .5); + shape += proportionOffset; } else { float shapeScaling = 5. * (1. - u_shapeScale); float e0 = 0.45 - shapeScaling; float e1 = 0.55 + shapeScaling; shape = smoothstep(min(e0, e1), max(e0, e1), 1.0 - uv.y + 0.3 * (proportion - 0.5)); } + return shape; +} +// Map the scalar pattern onto the color bands. u_softness sets band sharpness; aaW is +// a screen-space floor that antialiases individual band edges (needed when a pixel +// takes only 1 sample). Multi-crossing AA is handled by supersampling in main. +vec4 shadeBands(float shape, float aaW) { float mixer = shape * (u_colorsCount - 1.); vec4 gradient = u_colors[0]; gradient.rgb *= gradient.a; - float aa = fwidth(shape); - for (int i = 1; i < ${ warpMeta.maxColorCount }; i++) { + for (int i = 1; i < ${warpMeta.maxColorCount}; i++) { if (i >= int(u_colorsCount)) break; float m = clamp(mixer - float(i - 1), 0.0, 1.0); float localMixerStart = floor(m); - float softness = .5 * u_softness + fwidth(m); - float smoothed = smoothstep(max(0., .5 - softness - aa), min(1., .5 + softness + aa), m - localMixerStart); + float w = .5 * u_softness + aaW; + float e = m - localMixerStart; + float smoothed = w > 0. ? smoothstep(max(0., .5 - w), min(1., .5 + w), e) : step(.5, e); float stepped = localMixerStart + smoothed; m = mix(stepped, m, u_softness); @@ -147,11 +139,83 @@ void main() { c.rgb *= c.a; gradient = mix(gradient, c, m); } + return gradient; +} + + +// Contour band mask, locked to the borders. Smooth in screen space, so it is +// a reliable per-pixel signal (unlike derivatives of the folded warp). +float contourEdge(vec2 fragCoord) { + const float MAX_THICKNESS = .5; + vec2 borderUV = fragCoord / u_resolution; + vec2 mask = min(borderUV, 1. - borderUV); + vec2 pixel_thickness = min(400. * u_pixelRatio / u_resolution, vec2(MAX_THICKNESS)); + float maskX = pow(smoothstep(0.0, pixel_thickness.x, mask.x), .2); + float maskY = pow(smoothstep(0.0, pixel_thickness.y, mask.y), .2); + return clamp(1. - maskX * maskY, 0., 1.); +} + +vec2 warpUV(vec2 uv, vec2 fragCoord, float t) { + float edge = contourEdge(fragCoord); + + float n1 = valueNoise(uv * 1. + t); + float n2 = valueNoise(uv * 2. - t); + float angle = n1 * TWO_PI; + + float edgeFadeW = u_contour * edge; + float edgeFade = u_contour * pow(edge, 5.); + uv -= vec2(.5); + uv = rotate(uv, -edgeFade * angle); + uv += vec2(.5); + + uv.x += 4. * u_distortion * n2 * cos(angle) * (1. - edgeFadeW); + uv.y += 4. * u_distortion * n2 * sin(angle) * (1. - edgeFadeW); + + float swirl = u_swirl * (1. - edgeFadeW); + for (int i = 1; i <= 20; i++) { + if (i >= int(u_swirlIterations)) break; + float iFloat = float(i); + uv.x += swirl / iFloat * cos(t + iFloat * 1.5 * uv.y); + uv.y += swirl / iFloat * cos(t + iFloat * 1. * uv.x); + } + return uv; +} + + +void main() { + vec2 uv = v_patternUV * .5; + + const float firstFrameOffset = 118.; + float t = 0.0625 * (u_time + firstFrameOffset); + + vec4 acc; + if (u_antialiasing) { + vec2 duvdx = dFdx(uv); + vec2 duvdy = dFdy(uv); + const int AA_ROOT = 4; + acc = vec4(0.); + for (int i = 0; i < AA_ROOT * AA_ROOT; i++) { + int iy = i / AA_ROOT; + int ix = i - iy * AA_ROOT; + vec2 cell = (vec2(float(ix), float(iy)) + .5) / float(AA_ROOT) - .5; + // rotate ~26.57deg so the grid never resonates with the checks/stripes axes + vec2 o = vec2(cell.x * .8944 - cell.y * .4472, cell.x * .4472 + cell.y * .8944); + vec2 sBase = uv + o.x * duvdx + o.y * duvdy; + acc += shadeBands(patternShape(warpUV(sBase, gl_FragCoord.xy + o, t)), 0.); + } + acc /= float(AA_ROOT * AA_ROOT); + } else { + // Single sample + a cheap analytic floor that antialiases lone band edges. + vec2 wc = warpUV(uv, gl_FragCoord.xy, t); + float shapeC = patternShape(wc); + float aaW = min(fwidth(shapeC * (u_colorsCount - 1.)), .5); + acc = shadeBands(shapeC, aaW); + } - vec3 color = gradient.rgb; - float opacity = gradient.a; + vec3 color = acc.rgb; + float opacity = acc.a; - ${ colorBandingFix } + ${colorBandingFix} fragColor = vec4(color, opacity); } @@ -167,6 +231,8 @@ export interface WarpUniforms extends ShaderSizingUniforms { u_distortion: number; u_swirl: number; u_swirlIterations: number; + u_contour: number; + u_antialiasing: boolean; u_noiseTexture?: HTMLImageElement; } @@ -180,6 +246,8 @@ export interface WarpParams extends ShaderSizingParams, ShaderMotionParams { distortion?: number; swirl?: number; swirlIterations?: number; + contour?: number; + antialiasing?: boolean; } export const WarpPatterns = {