// The moire wallpaper's whole picture: ground, bokeh, halftone dots and
// moire rings, the patterns fading out into the ground away from a
// drifting hub.
//
// Everything that moves is computed in Field.qml, in double precision,
// and arrives here as positions and phases; the shader holds no clock,
// so it never loses precision however long the session runs.
//
// Compiled into ../theme/contents/ui/field.frag.qsb by tools/shaders.sh.
#version 440
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
vec2 size; // pixels
// Positions and lengths below are in units of the height, so every
// aspect ratio shows the same pattern at the same apparent scale.
vec2 ringA; // centres of the two ring families
vec2 ringB;
vec2 hub; // where the pattern is strongest
float reach; // distance at which it has faded out entirely
float period; // ring spacing
float moire; // 0-1, how much of the rings to show
float halftone; // 0-1, how much of the dots to show
float cell; // halftone grid spacing
float angle; // halftone grid rotation, radians
float wave; // halftone wave phase, radians, kept in [0, 2pi)
float bokeh; // 0-1, how much of the bokeh to show
float drift; // bokeh phase, 0-1 over its whole cycle
vec4 base; // the ground
vec4 deep; // the ground at the edges
vec4 ink; // the moire
vec4 tone; // the halftone
};
float fade(vec2 p)
{
float t = clamp(1.0 - length(p - hub) / reach, 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
float rings(vec2 p, vec2 centre, float px)
{
float r = length(p - centre);
float d = abs(fract(r / period + 0.5) - 0.5) * period;
float halfWidth = max(0.9 * px, period * 0.09);
return clamp(1.0 - (d - halfWidth) / px, 0.0, 1.0);
}
// A repeatable pseudo-random number in [0, 1) per disc and purpose.
float hash(float n)
{
return fract(sin(n * 127.1 + 311.7) * 43758.5453);
}
// Out-of-focus lights drifting upward. Each disc moves at a whole
// multiple of drift, so when drift wraps from 1 back to 0 every disc
// is exactly where it started; each fades in rising from the bottom
// and out before the top, so none ever appears or vanishes abruptly.
const int DISCS = 14;
float bokehLight(vec2 p, float aspect, float px, out float tint)
{
float total = 0.0;
tint = 0.0;
for (int i = 0; i < DISCS; i++) {
float n = float(i);
float climb = 1.0 + floor(hash(n + 1.0) * 3.0); // 1-3 laps a cycle
float sway = 1.0 + floor(hash(n + 2.0) * 2.0);
float life = fract(hash(n + 3.0) + climb * drift); // 0 at bottom, 1 at top
vec2 c = vec2(hash(n + 4.0) * aspect
+ 0.04 * sin(6.2831853 * (sway * drift + hash(n + 5.0))),
1.15 - 1.3 * life);
float radius = 0.035 + 0.075 * hash(n + 6.0);
float d = length(p - c);
float disc = clamp((radius - d) / (2.5 * px) + 0.5, 0.0, 1.0);
// A lens renders a defocused point with a slightly brighter rim.
float rim = smoothstep(radius * 0.7, radius, d) * disc;
float shape = disc * 0.65 + rim * 0.35;
float strength = shape * sin(3.1415927 * life) * (0.45 + 0.55 * hash(n + 7.0));
total += strength;
tint += strength * step(0.5, hash(n + 8.0));
}
tint = total > 0.0 ? tint / total : 0.0;
return min(total, 1.0);
}
void main()
{
float px = 1.0 / size.y;
vec2 p = qt_TexCoord0 * size * px;
vec2 centre = vec2(0.5 * size.x * px, 0.5);
// Ground: base, darkening towards deep at the corners.
float v = smoothstep(0.35, 1.25, length(p - centre));
vec3 colour = mix(base.rgb, deep.rgb, v);
if (bokeh > 0.0) {
float tint;
float light = bokehLight(p, size.x * px, px, tint);
colour = mix(colour, mix(ink.rgb, tone.rgb, tint), light * 0.3 * bokeh);
}
// Halftone: dots on a rotated grid, each sized by the fade and a
// travelling wave sampled at its cell's centre, so a dot is round
// and uniform rather than cut by the field. Area, not radius, is
// proportional to the field, as in print.
if (halftone > 0.0) {
mat2 rot = mat2(cos(angle), sin(angle), -sin(angle), cos(angle));
vec2 q = rot * p;
vec2 index = floor(q / cell);
vec2 local = (q / cell - index - 0.5) * cell;
vec2 cellCentre = transpose(rot) * ((index + 0.5) * cell);
float field = fade(cellCentre)
* (0.55 + 0.45 * sin(dot(cellCentre, vec2(0.8, 0.6)) * 9.0 - wave));
float radius = 0.5 * cell * sqrt(field);
float dotCover = clamp((radius - length(local)) / px + 0.5, 0.0, 1.0);
colour = mix(colour, tone.rgb, dotCover * halftone * 0.55);
}
// Moire: two ring families slightly apart. Where their rings
// interleave the light doubles up, where they coincide it thins
// out, and that beat is the moire.
if (moire > 0.0) {
float a = rings(p, ringA, px);
float b = rings(p, ringB, px);
float light = 1.0 - (1.0 - 0.85 * a) * (1.0 - 0.85 * b);
colour = mix(colour, ink.rgb, light * 0.6 * fade(p) * moire);
}
fragColor = vec4(colour, 1.0) * qt_Opacity;
}
neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
5.3 KiB · text
History
c3df760