fork a shader
Write or paste an ISF shader (GLSL ES with a /*{ ... }*/ header). It compiles + renders in yer browser. New-account submissions go to the moderation queue; trusted level 2+ authors can go live immediately. Everything is CC0. Got a file first? preview it locally →
A model marks this as witnessed_generation (honest AI provenance). Leave blank if you hand-authored it.
/*{ "DESCRIPTION": "A shower of CDs and 3.5 inch floppies falling and tumbling in the dark, with the rainbow on every disc solved from the grating equation rather than painted with a hue ramp. The data spiral is a real reflection grating: both the view ray and the light ray are refracted into the polycarbonate (n = 1.58) and walked down the 1.2mm to the metal, and with u the in-plane radial direction and g the tangential groove direction, the component across the grooves gives lambda = n*pitch*|q|/m while the component along them has to vanish for the order to exist at all. That second condition is what puts the rainbow in two opposed lobes; the lamp's angular size is what gives them thickness instead of leaving them curves of measure zero. The bands stack outward because when the first order has run past red into the infrared the second is arriving at blue -- which is what a CD does and what a hue ramp cannot. Colour is the CIE 1931 matching functions, so every wavelength gets the colour it really has, and the lobes are widened by the wavelength bandwidth one pixel covers, so where the bands crowd tighter than the pixels can follow they wash out to white instead of aliasing into confetti. Each object is a torque-free symmetric top with I3 = 2*I1, so its axis precesses about the angular momentum at twice the rate the body turns beneath it and the other way round: Feynman's plate. Because the grating is rotationally symmetric, the body spin moves only the scratches, the thumbprint and the write spot, while the normal swinging round its cone is what sweeps the colour. The same law carries the floppies, and that is not a fudge -- the perpendicular axis theorem gives a flat plate I3 = I1 + I2, so a square plate has I3 = 2*I1 exactly, and 90 by 94 is within four percent of square. Every rate is a whole number of turns per minute, so the whole shower is an exact 60 second loop. Around that: dye tint applied on the way in and out, the mirror band, lead-out and clear rim, arc scratches and wipe marks, a thumbprint that fogs the grating rather than dimming it, dust, and the burn boundary walking outward with a write spot riding it. The floppies are a box and a half-space for the cut corner, with shutter, hub, drive slot and corner holes. Twenty-two printed disc faces from 1988 to 2005 and nine floppy labels -- all of them period STYLES, not copies of anybody's real logo. None of the lettering is lettering either: dashed arcs read as microtext, and a stroke with a sloping baseline, per-letter pen pressure and proper risers and tails reads as handwriting. Three liberties, all noted in the source: the write laser is really 780nm infrared and invisible, the spindle is slowed so the spot reads at 60fps, and the key light is rigged to each object rather than to the room -- one lamp cannot be near-field to a shower half a metre wide, and near-field is exactly what makes the orders stack. Every knob exposed.", "CREDIT": "CC0", "ISFVSN": "2", "CATEGORIES": ["generator"], "INPUTS": [ { "NAME": "uSpeed", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 3.0, "LABEL": "speed" }, { "NAME": "uCamPitch", "TYPE": "float", "DEFAULT": 14.0, "MIN": 5.0, "MAX": 85.0, "LABEL": "camera pitch" }, { "NAME": "uCamYaw", "TYPE": "float", "DEFAULT": 0.35, "MIN": -3.15, "MAX": 3.15, "LABEL": "camera yaw" }, { "NAME": "uCamDist", "TYPE": "float", "DEFAULT": 480.0, "MIN": 200.0, "MAX": 1600.0, "LABEL": "camera distance" }, { "NAME": "uFov", "TYPE": "float", "DEFAULT": 50.0, "MIN": 18.0, "MAX": 70.0, "LABEL": "field of view" }, { "NAME": "uLampHeight", "TYPE": "float", "DEFAULT": 92.0 , "MIN": 40.0, "MAX": 600.0, "LABEL": "lamp height" }, { "NAME": "uLampRadius", "TYPE": "float", "DEFAULT": 60.0 , "MIN": 0.0, "MAX": 400.0, "LABEL": "lamp radius" }, { "NAME": "uLampOrbit", "TYPE": "float", "DEFAULT": 1.00, "MIN": -6.0, "MAX": 6.0, "LABEL": "lamp orbits/min" }, { "NAME": "uSourceSize", "TYPE": "float", "DEFAULT": 0.140, "MIN": 0.005, "MAX": 0.35, "LABEL": "lamp size" }, { "NAME": "uLampPower", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 4.0, "LABEL": "lamp power" }, { "NAME": "uPitch", "TYPE": "float", "DEFAULT": 1.60, "MIN": 0.35, "MAX": 2.20, "LABEL": "track pitch um" }, { "NAME": "uOrders", "TYPE": "float", "DEFAULT": 3.0, "MIN": 1.0, "MAX": 3.0, "LABEL": "orders" }, { "NAME": "uDye", "TYPE": "long", "DEFAULT": 4, "VALUES": [0, 1, 2, 3, 4], "LABELS": ["azo (green)", "cyanine (blue)", "phthalocyanine (gold)", "pressed (silver)", "mixed"], "LABEL": "dye" }, { "NAME": "uCount", "TYPE": "float", "DEFAULT": 28.0, "MIN": 1.0, "MAX": 28.0, "LABEL": "objects" }, { "NAME": "uFloppy", "TYPE": "float", "DEFAULT": 0.40, "MIN": 0.0, "MAX": 1.0, "LABEL": "floppies" }, { "NAME": "uFall", "TYPE": "float", "DEFAULT": 6.0, "MIN": 0.0, "MAX": 30.0, "LABEL": "falls/min" }, { "NAME": "uSpread", "TYPE": "float", "DEFAULT": 450.0, "MIN": 320.0, "MAX": 900.0, "LABEL": "spread" }, { "NAME": "uBurn", "TYPE": "float", "DEFAULT": 0.62, "MIN": 0.0, "MAX": 1.0, "LABEL": "burn" }, { "NAME": "uBurnSpeed", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.0, "MAX": 8.0, "LABEL": "burns/min" }, { "NAME": "uWriteGlow", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 2.0, "LABEL": "write spot" }, { "NAME": "uScratches", "TYPE": "float", "DEFAULT": 0.45, "MIN": 0.0, "MAX": 1.0, "LABEL": "scratches" }, { "NAME": "uScuff", "TYPE": "float", "DEFAULT": 0.40, "MIN": 0.0, "MAX": 1.0, "LABEL": "scuff" }, { "NAME": "uSmudge", "TYPE": "float", "DEFAULT": 0.35, "MIN": 0.0, "MAX": 1.0, "LABEL": "thumbprint" }, { "NAME": "uDust", "TYPE": "float", "DEFAULT": 0.30, "MIN": 0.0, "MAX": 1.0, "LABEL": "dust" }, { "NAME": "uWarp", "TYPE": "float", "DEFAULT": 0.35, "MIN": 0.0, "MAX": 1.0, "LABEL": "warp" }, { "NAME": "uWobble", "TYPE": "float", "DEFAULT": 54.0, "MIN": 0.0, "MAX": 80.0, "LABEL": "wobble deg" }, { "NAME": "uPrecess", "TYPE": "float", "DEFAULT": 8.0 , "MIN": 0.0, "MAX": 60.0, "LABEL": "wobbles/min" }, { "NAME": "uAniso", "TYPE": "float", "DEFAULT": 0.75, "MIN": 0.0, "MAX": 1.0, "LABEL": "streak" }, { "NAME": "uGloss", "TYPE": "float", "DEFAULT": 0.60, "MIN": 0.0, "MAX": 1.0, "LABEL": "gloss" }, { "NAME": "uGain", "TYPE": "float", "DEFAULT": 1.50, "MIN": 0.0, "MAX": 3.0, "LABEL": "spectrum gain" }, { "NAME": "uAmbient", "TYPE": "float", "DEFAULT": 0.12, "MIN": 0.0, "MAX": 1.0, "LABEL": "ambient" }, { "NAME": "uExposure", "TYPE": "float", "DEFAULT": 1.05, "MIN": 0.0, "MAX": 3.0, "LABEL": "exposure" }, { "NAME": "uBloom", "TYPE": "float", "DEFAULT": 0.62, "MIN": 0.0, "MAX": 1.0, "LABEL": "bloom" }, { "NAME": "uVignette", "TYPE": "float", "DEFAULT": 0.40, "MIN": 0.0, "MAX": 1.0, "LABEL": "vignette" } ] }*/ #define PI 3.14159265359 #define TAU 6.28318530718 #define SPIN (TAU * 53.0 / 60.0) // spindle, turns per loop #define NDISC 28 // Disc geometry, millimetres. A 120mm disc: 15mm hole, clamping // area out to ~33mm diameter, the metallised layer starting at // the mirror band, program area 50-116mm diameter, then lead-out // and a clear rim. #define R_OUT 60.0 #define R_HOLE 7.5 #define R_CLAMP 22.0 // clear polycarbonate ends, metal begins #define R_MIRROR 25.0 // ungrooved mirror band ends, data begins #define R_DATA 58.0 // program area ends #define R_RIM 58.6 // lead-out ends, clear rim begins #define TH 1.2 // polycarbonate thickness read through #define NPC 1.58 // refractive index of polycarbonate #define ETA (1.0 / NPC) // State of the disc currently being shaded. A mutable global spares // threading it through every shading function; it is set once, in // the entry point, after the nearest disc has been found. mat3 gM; // disc space -> world, for the disc being shaded float gLabel; // which printed face it carries float gDye; // which recording dye float gBurn; // where its burn started float gKind; // 0 a disc, 1 a floppy // ---- hashes and a little value noise ---- float hash11(float p){ p = fract(p * 0.1031); p *= p + 33.33; p *= p + p; return fract(p); } float hash12(vec2 p){ vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); } float vnoise(vec2 p){ vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f); return mix(mix(hash12(i), hash12(i + vec2(1.0, 0.0)), f.x), mix(hash12(i + vec2(0.0, 1.0)), hash12(i + vec2(1.0, 1.0)), f.x), f.y); } // wrap an angle difference into [-PI, PI] without a modulo float wrapPi(float a){ return a - TAU * floor(a / TAU + 0.5); } // ---- CIE 1931 colour matching, analytic and blurrable ---- // Wyman, Sloan and Shirley multi-lobe piecewise Gaussians. The // extra argument convolves every lobe with a Gaussian of that // width, which is how a pixel that covers a wide band of // wavelengths gets to desaturate instead of alias. Widening a // Gaussian and keeping its integral means scaling the peak by // s/sqrt(s*s + b*b), which is the factor out front. float pgB(float x, float mu, float s1, float s2, float b){ float s = (x < mu) ? s1 : s2; float w = sqrt(s * s + b * b); float t = (x - mu) / w; return (s / w) * exp(-0.5 * t * t); } vec3 xyzBar(float w, float b){ return vec3( 1.056 * pgB(w, 599.8, 37.9, 31.0, b) + 0.362 * pgB(w, 442.0, 16.0, 26.7, b) - 0.065 * pgB(w, 501.1, 20.4, 26.2, b), 0.821 * pgB(w, 568.8, 46.9, 40.5, b) + 0.286 * pgB(w, 530.9, 16.3, 31.1, b), 1.217 * pgB(w, 437.0, 11.8, 36.0, b) + 0.681 * pgB(w, 459.0, 26.0, 13.8, b)); } vec3 xyz2rgb(vec3 c){ return vec3( 3.2406 * c.x - 1.5372 * c.y - 0.4986 * c.z, -0.9689 * c.x + 1.8758 * c.y + 0.0415 * c.z, 0.0557 * c.x - 0.2040 * c.y + 1.0570 * c.z); } // A single wavelength sits outside sRGB, so the naive transform // comes back with a negative channel. Adding just enough white // to clear it is the standard desaturate-into-gamut move, and it // keeps the spectrum vivid instead of clipping it to mud. vec3 toGamut(vec3 rgb){ float m = min(min(rgb.r, rgb.g), rgb.b); if (m < 0.0) rgb -= m; return max(rgb, 0.0); } // ---- the lamp ---- vec3 lampPos(float t){ float a = t * uLampOrbit * TAU / 60.0 + 2.35; return vec3(cos(a) * uLampRadius, uLampHeight, sin(a) * uLampRadius); } // ---- surface detail ---- // The height gradient of everything that is not the data spiral // -- warp, stacking ring, scratches, thumbprint -- plus broadband // scatter and how far the grating is locally decohered (a smudge // fogs the rainbow, it does not just dim it). void surfaceDetail(vec2 pd, float r, float th, float fp, out vec3 N, out float scatter, out float decoh, out float rough){ vec2 g = vec2(0.0); scatter = 0.0; decoh = 0.0; rough = 0.0; // The disc is not flat. Two low-frequency lobes, differentiated // by hand so the normal is exact rather than sampled. A few // hundredths of a radian is enough: the band position moves by // 2*slope in q, which is more than one band width. float A = uWarp * 2.0; float sx = sin(0.031 * pd.x + 1.3), cx = cos(0.031 * pd.x + 1.3); float sz = sin(0.027 * pd.y - 0.7), cz = cos(0.027 * pd.y - 0.7); float s2 = sin(0.017 * pd.x - 0.9), c2 = cos(0.017 * pd.x - 0.9); float s3 = sin(0.021 * pd.y + 2.2), c3 = cos(0.021 * pd.y + 2.2); g.x += A * (0.031 * cx * cz + 0.55 * 0.017 * c2 * s3); g.y += A * (-0.027 * sx * sz + 0.55 * 0.021 * s2 * c3); // the moulded stacking ring in the clamping area float dsr = r - 18.25; float ring = 0.075 * exp(-dsr * dsr / 1.96); float dring = -2.0 * dsr / 1.96 * ring; vec2 uhF = (r > 1e-4) ? pd / r : vec2(1.0, 0.0); // radial, flat vec2 ghF = vec2(-uhF.y, uhF.x); // tangential, flat g += uhF * dring; // Arc scratches. The groove of a circumferential scratch runs // tangentially, so its cross-section slope is purely radial and // comes out in closed form from the Gaussian profile — no finite // differences, no extra noise lookups. for (int i = 0; i < 10; i++){ float fi = float(i); if (hash11(fi * 5.51 + 3.0) > uScratches + 0.12) continue; float rc = mix(12.0, 59.0, hash11(fi * 3.71 + 11.0)); float ac = hash11(fi * 7.13 + 5.0) * TAU; float aw = mix(0.18, 1.7, hash11(fi * 2.37 + 19.0)); float w0 = mix(0.10, 0.42, hash11(fi * 1.90 + 2.0)); // A scratch narrower than a pixel must not be drawn narrower than a // pixel: sampled sharp it breaks into a dashed line. Widen it to the // footprint and dim it by the same factor, which is what filtering it // properly would have done. float w = max(w0, fp * 0.9); float da = wrapPi(th - ac); float am = smoothstep(aw, aw * 0.5, abs(da)); if (am < 0.003) continue; float dr = r - rc + 1.1 * sin(da * 2.7 + fi); float e = exp(-dr * dr / (w * w)); float k = w0 / w; g += uhF * (-2.0 * dr / (w * w)) * e * 0.012 * am; scatter += e * am * 0.55 * k; decoh += e * am * 0.35 * k; } // Radial wipe marks. Same trick, rotated: the cross-section // coordinate is arc length, so the slope lands on the tangent. for (int i = 0; i < 6; i++){ float fi = float(i); if (hash11(fi * 9.13 + 41.0) > uScratches * 0.75) continue; float ac = hash11(fi * 4.41 + 7.0) * TAU; float w0 = mix(0.09, 0.30, hash11(fi * 6.17 + 13.0)); float w = max(w0, fp * 0.9); float r0 = mix(10.0, 34.0, hash11(fi * 2.71 + 23.0)); float r1 = r0 + mix(9.0, 34.0, hash11(fi * 8.09 + 31.0)); float rm = smoothstep(r0, r0 + 3.0, r) * smoothstep(r1, r1 - 3.0, r); if (rm < 0.003) continue; float dl = r * wrapPi(th - ac); float e = exp(-dl * dl / (w * w)); float k = w0 / w; g += ghF * (-2.0 * dl / (w * w)) * e * 0.010 * rm; scatter += e * rm * 0.45 * k; decoh += e * rm * 0.30 * k; } // Fine circumferential scuff. These are far below a pixel, so // they arrive as roughness and a milky sheen, not as normals. float sc = vnoise(vec2(r * 1.1, th * 9.0)) * 0.65 + vnoise(vec2(r * 4.3, th * 21.0)) * 0.35; rough += uScuff * (0.25 + 0.75 * sc); scatter += uScuff * sc * 0.09; // A thumbprint. The ridges tilt the surface, and the grease // between them scrambles the grating: decoh widens both the // conical acceptance and the wavelength blur further down, so // the rainbow goes pale under it instead of merely dimmer. vec2 df = pd - vec2(24.0, -21.0); float rf = length(df); float sm = uSmudge * exp(-rf * rf / 361.0); if (sm > 0.002){ float ang = atan(df.y, df.x); float rgF = 11.4; // ridges about 0.55mm apart float ph = rf * rgF + 2.2 * sin(ang * 1.7 + 0.7) + 1.3 * sin(rf * 0.35); float dph = rgF + 0.455 * cos(rf * 0.35); float fpd = fp * rgF; float damp = 1.0 / (1.0 + 4.0 * fpd * fpd); g += (df / max(rf, 1e-4)) * (-0.0028 * sm * cos(ph) * dph * damp); decoh += sm * 0.60; scatter += sm * 0.12; rough += sm * 0.5; } // Dust: sparse, in cells about a millimetre across. vec2 ci = floor(pd * 0.85); if (hash12(ci) > 1.0 - 0.11 * uDust){ vec2 c = vec2(hash12(ci + 7.1), hash12(ci + 3.3)); float d = length(fract(pd * 0.85) - c); float sz2 = mix(0.06, 0.17, hash12(ci + 11.0)); float sp = smoothstep(sz2, sz2 * 0.35, d); scatter += sp * 1.1; decoh += sp * 0.9; } N = normalize(vec3(-g.x, 1.0, -g.y)); } // ---- the grating equation ---- // Returns (q, s, data-layer x, data-layer z). Both rays are bent // into the polycarbonate first and the view ray is walked down // the 1.2mm to the metal, so the point the spectrum is read at is // not the point the eye is looking at — that offset is the real // parallax between the surface highlight and the rainbow. vec4 gratingQS(vec3 p, vec3 rd, vec3 N, vec3 lp){ vec3 Vi = refract(rd, N, ETA); vec3 pd3 = p + Vi * (TH / max(-Vi.y, 0.02)); vec3 Li = refract(-normalize(lp - p), N, ETA); vec3 Vo = -Vi, Lo = -Li; vec3 rad = vec3(pd3.x, 0.0, pd3.z); if (dot(rad, rad) < 1e-6) rad = vec3(1.0, 0.0, 0.0); vec3 gh = normalize(cross(N, rad)); // groove direction, tangential vec3 uh = cross(gh, N); // across the grooves, radial return vec4(dot(Lo, uh) + dot(Vo, uh), dot(Lo, gh) + dot(Vo, gh), pd3.x, pd3.z); } // ---- the spectrum ---- // dq and ds are the footprint of one pixel in (q, s). ds widens // the conical acceptance; dq becomes the wavelength bandwidth the // pixel covers, which is fed straight into the CIE lobes. That is // the whole anti-aliasing story: where the bands crowd together // faster than the pixels can follow, they desaturate to white on // their own rather than turning to confetti. vec3 spectrumXYZ(float q, float s, float dq, float ds, float pitchNm, float decoh){ float sig = sqrt(uSourceSize * uSourceSize + ds * ds + decoh * decoh * 0.010); float cone = exp(-0.5 * (s * s) / (sig * sig)); if (cone < 0.0015) return vec3(0.0); float nd = NPC * pitchNm; float aq = abs(q); float blur0 = nd * (dq + decoh * 0.10) + 4.0; vec3 xyz = vec3(0.0); for (int m = 1; m <= 3; m++){ float fm = float(m); if (fm > uOrders + 0.5) break; float lam = nd * aq / fm; float blur = blur0 / fm; if (lam < 330.0 - blur || lam > 820.0 + blur) continue; xyz += xyzBar(lam, blur) * pow(0.38, fm - 1.0); } return xyz * cone; } // ---- lobes ---- // Ward, anisotropic. The grooves make the surface smooth along g // and corrugated across u, so the highlight stretches along u — // the zeroth order on a CD is a radial streak, not a dot. float wardAniso(vec3 Lo, vec3 Vo, vec3 N, vec3 uh, vec3 gh, float ax, float ay){ vec3 h = Lo + Vo; float hl = length(h); if (hl < 1e-4) return 0.0; h /= hl; float hn = dot(h, N); if (hn < 1e-3) return 0.0; float hu = dot(h, uh) / hn; float hg = dot(h, gh) / hn; return exp(-(hu * hu / (ax * ax) + hg * hg / (ay * ay))) / (4.0 * PI * ax * ay); } float ggx(vec3 L, vec3 V, vec3 N, float a){ vec3 h = normalize(L + V); float nh = max(dot(N, h), 0.0); float d = nh * nh * (a * a - 1.0) + 1.0; return a * a / (PI * d * d + 1e-6); } // Round-trip transmission through the recording dye, and what the // written region does to it. A pressed disc has no dye at all. vec4 dyeLayer(float burned){ if (gDye > 2.5) return vec4(1.0); vec3 fresh = (gDye < 0.5) ? vec3(0.62, 0.93, 0.71) // azo, green : (gDye < 1.5) ? vec3(0.66, 0.88, 1.00) // cyanine, blue-green : vec3(1.00, 0.94, 0.72); // phthalocyanine, faint gold return vec4(mix(fresh, fresh * vec3(0.86, 0.75, 0.67), burned), mix(1.0, 0.84, burned)); } // ---- the studio ---- // A FLAT mirror shows only a narrow cone of directions, so light // it with smooth gradients and every pixel lands on the same patch // of gradient: the disc comes back one flat colour and reads as // frosted plastic. It was never the environment's STRENGTH that // was wrong, it had no structure at the only angular scale a flat // mirror can resolve. Hence ceiling strip lights, narrow enough // that a disc catches a bar or two and sweeps the rest as it // tumbles. float ceilingStrips(vec3 d){ if (d.y < 0.05) return 0.0; vec3 dn = d / max(d.y, 0.05); // onto the ceiling plane float u = (dn.x + dn.z) * 0.7071; // across the tubes float v = (dn.x - dn.z) * 0.7071; // along them float g = 0.0; for (int i = 0; i < 5; i++){ float t = u - (float(i) - 2.0) * 0.30; g += exp(-t * t / 0.0060); } return g * smoothstep(2.6, 1.1, abs(v)) * smoothstep(0.05, 0.26, d.y); } vec3 studioEnv(vec3 d){ float h = clamp(d.y * 0.8 + 0.5, 0.0, 1.0); vec3 c = mix(vec3(0.006, 0.006, 0.009), vec3(0.026, 0.028, 0.038), h * h); c += vec3(1.00, 0.98, 0.94) * ceilingStrips(d) * 0.34; c += vec3(0.24, 0.25, 0.31) * smoothstep(0.60, 0.02, abs(d.y)); return c * (0.30 + uAmbient * 3.2); } // Behind the discs is nothing at all. The room they REFLECT is // still there in studioEnv -- what a set separates is the paper // behind the subject from the panels lighting it, and here the // paper is simply black. vec3 planeHit(vec3 ro, vec3 rd){ return ro + rd * ((TH * 0.5 - ro.y) / min(rd.y, -1e-4)); } // ---- the disc ---- // Everything from here down is in DISC SPACE: +y is the disc's // own normal and the body turns with it, so the scratches, the // thumbprint and the burn all sit still in these coordinates // while the disc tumbles. That is not a shortcut, it is the // point -- the grating is rotationally symmetric, so spinning // the disc moves the scratches and nothing else. All the motion // in the rainbow comes from the normal changing direction. // // rdx / rdy are the ray directions one pixel over; the mirrored // pair is reconstructed from them, so the footprint gets both a // first and a second difference. vec3 shadeDisc(vec3 ro, vec3 rd, vec3 rdx, vec3 rdy, vec3 lp, float t, out vec3 glow){ glow = vec3(0.0); vec3 p = planeHit(ro, rd); vec3 px = planeHit(ro, rdx); vec3 py = planeHit(ro, rdy); float fp = 0.5 * (length(px.xz - p.xz) + length(py.xz - p.xz)); vec3 N; float scatter, decoh, rough; float r0 = length(p.xz); surfaceDetail(p.xz, r0, atan(p.z, p.x), fp, N, scatter, decoh, rough); vec4 qs = gratingQS(p, rd, N, lp); vec2 pd = qs.zw; float r = length(pd); float th = atan(pd.y, pd.x); // Footprint, against the FLAT normal: what is wanted is the // geometric rate of change of (q, s) across a pixel, and four // more surfaceDetail calls would fold the scratches into their // own blur. The second difference is not optional -- at a // stationary point of q the first one vanishes while the field // is still moving under the pixel, so the linear estimate // alone lets exactly the busiest region alias. vec3 FLAT = vec3(0.0, 1.0, 0.0); vec3 rmx = normalize(2.0 * rd - rdx); vec3 rmy = normalize(2.0 * rd - rdy); vec4 c0 = gratingQS(p, rd, FLAT, lp); vec4 cxp = gratingQS(px, rdx, FLAT, lp); vec4 cyp = gratingQS(py, rdy, FLAT, lp); vec4 cxm = gratingQS(planeHit(ro, rmx), rmx, FLAT, lp); vec4 cym = gratingQS(planeHit(ro, rmy), rmy, FLAT, lp); float dq = 0.5 * (abs(cxp.x - cxm.x) + abs(cyp.x - cym.x)) + 0.5 * (abs(cxp.x + cxm.x - 2.0 * c0.x) + abs(cyp.x + cym.x - 2.0 * c0.x)); float ds = 0.5 * (abs(cxp.y - cym.y) + abs(cyp.y - cym.y)) + 0.5 * (abs(cxp.y + cxm.y - 2.0 * c0.y) + abs(cyp.y + cym.y - 2.0 * c0.y)); float w = max(fp * 0.7, 0.03); // ---- radial zones ---- float mMetal = smoothstep(R_CLAMP - w, R_CLAMP + w, r) * smoothstep(R_RIM + w, R_RIM - w, r); float mData = smoothstep(R_MIRROR - w, R_MIRROR + w, r) * smoothstep(R_DATA + w, R_DATA - w, r); float mLead = smoothstep(R_DATA - w, R_DATA + w, r) * smoothstep(R_RIM + w, R_RIM - w, r); float grating = mData + 0.5 * mLead; // the mirror band has no grooves // ---- the burn ---- float phase = fract(gBurn + t * uBurnSpeed / 60.0); float rBurn = mix(R_MIRROR, R_DATA, phase); float burned = smoothstep(rBurn + max(w, 0.22), rBurn - max(w, 0.22), r) * mData; vec4 dye = dyeLayer(burned); // ---- light ---- vec3 dl = lp - p; float d2 = dot(dl, dl); vec3 L = dl * inversesqrt(d2); float irr = uLampPower * 30000.0 / max(d2, 1.0); float ndl = max(dot(N, L), 0.0); vec3 Vi = refract(rd, N, ETA), Li = refract(-L, N, ETA); vec3 Vo = -Vi, Lo = -Li; vec3 rad = vec3(pd.x, 0.0, pd.y); if (dot(rad, rad) < 1e-6) rad = vec3(1.0, 0.0, 0.0); vec3 gh = normalize(cross(N, rad)); vec3 uh = cross(gh, N); // ---- the diffracted orders ---- float pitchNm = uPitch * 1000.0; vec3 spec = toGamut(xyz2rgb(spectrumXYZ(qs.x, qs.y, dq, ds, pitchNm, decoh))) * 0.62; // ---- the zeroth order ---- // Rough across the grooves, smooth along them, so the specular // reflection of the lamp arrives as a radial streak. float ax = mix(0.040, 0.13, uAniso) + rough * 0.060; float ay = mix(0.055, 0.014, uAniso) + rough * 0.030; float zer = wardAniso(Lo, Vo, N, uh, gh, ax, ay) * ndl; vec3 alu = vec3(0.94, 0.95, 0.97); // ---- the write spot ---- // The head is fixed in space while the disc turns under it, so // in these body coordinates it is the spot that moves. vec3 write = vec3(0.0); if (uWriteGlow > 0.002){ float aw = -t * SPIN; float da = wrapPi(th - aw); float dr = r - rBurn; float radial = exp(-dr * dr / 0.55); float head = exp(-da * da * 900.0); float tail = (da > 0.0) ? exp(-da * 2.4) : 0.0; write = (vec3(1.00, 0.40, 0.16) * head * 2.6 + vec3(1.00, 0.52, 0.22) * tail * 0.22) * radial * uWriteGlow * mData; } // ---- the polycarbonate front face ---- vec3 envR = studioEnv(gM * reflect(rd, N)); float fres = 0.05 + 0.95 * pow(1.0 - max(dot(N, -rd), 0.0), 5.0); float topA = mix(0.020, 0.14, 1.0 - uGloss) + rough * 0.050; float top = ggx(L, -rd, N, topA) * ndl * fres; // ---- broadband scatter off scratches and dust ---- float broad = pow(max(dot(reflect(-L, N), -rd), 0.0), 6.0); float scat = scatter * (0.20 + broad * 2.6) * ndl; // ---- put the disc together ---- vec3 metalCol = ((spec * grating * uGain * ndl * 1.35 + alu * zer * 0.038) * irr + alu * envR) * dye.rgb * dye.a + write; // Where there is no metal the disc is a window: clear // polycarbonate with the backdrop behind it. vec3 clearCol = envR * 0.09; // clear plastic over a black backdrop vec3 col = mix(clearCol, metalCol, mMetal); col += envR * fres * 0.55; col += vec3(0.97, 0.97, 1.00) * top * irr * 0.075; col += vec3(1.00, 0.99, 0.96) * scat * irr * 0.030; // Veiling glare: the same two lobes evaluated far wider. It is // a real spatial spread computed per pixel, which is the only // kind available without a blur pass. if (uBloom > 0.002){ float gz = wardAniso(Lo, Vo, N, uh, gh, ax * 4.5 + 0.25, ay * 6.0 + 0.13) * ndl; float gt = ggx(L, -rd, N, 0.62) * ndl * fres * 0.5; vec3 gs = toGamut(xyz2rgb(spectrumXYZ(qs.x, qs.y, dq, ds * 2.6, pitchNm, decoh + 0.35))) * 0.62; glow = (gs * grating * uGain * 0.12 * dye.rgb + alu * gz * 0.038 + vec3(0.97, 0.97, 1.0) * gt * 0.075) * irr + write * 0.5; // The glare picks up glints off the same dust and scratches that // scattered the light to begin with, so it breaks into points // rather than arriving as a smooth wash. Seeded in the disc's own // frame, so they ride the body spin, widened by the footprint so // they cannot alias, and twinkling a whole number of times a // minute so the loop still closes. vec2 sg = pd * 0.15; // ~7mm cells, so a glint is a glint vec2 si = floor(sg); vec2 sc = vec2(hash12(si + 7.7), hash12(si + 4.1)) - 0.5; float sd = length(fract(sg) - 0.5 - sc * 0.6); float tw = 0.55 + 0.45 * sin(TAU * 13.0 * t / 60.0 + hash12(si) * 44.0); glow *= 1.0 + step(0.74, hash12(si + 2.3)) * tw * smoothstep(0.23 + fp * 0.06, 0.02, sd) * 4.5; } return col; } // ---- the printed sides ---- // Faces from 1988 to 2005. The artwork on a CD-R sits above the // reflective layer, so a face is only in shot when its tumble // carries it past edge-on -- which is most of them, most of the // time, once it is raining. // // None of the lettering is lettering: dashed arcs read as // microtext, a wobbling stroke broken into runs reads as // handwriting. Both sit on ARCS, spaced in arc length so the pitch // stays even -- a label turns with its disc, and a rectangular // block of type looks arbitrary at every angle that puts it // through, while type on an arc looks deliberate at all of them. float dashRing(float r, float a, float rc, float w, float n, float duty, float fp){ float band = smoothstep(w + fp, w - fp * 0.5, abs(r - rc)); float d = fract(a * n / TAU); return band * smoothstep(duty + 0.10, duty - 0.10, d); } // two overlapping discs, one cutting the other: the swoosh float crescent(vec2 p, vec2 c1, float r1, vec2 c2, float r2, float aa){ return smoothstep(r1 + aa, r1 - aa, length(p - c1)) * smoothstep(r2 - aa, r2 + aa, length(p - c2)); } // Frutiger bubbles, with the rim highlight that made them bubbles float bubbles(vec2 p, float seed, float fp, out float rim){ float m = 0.0; rim = 0.0; for (int i = 0; i < 12; i++){ float fi = float(i) + seed; vec2 c = vec2(hash11(fi * 3.11 + 1.0), hash11(fi * 7.77 + 4.0)) * 104.0 - 52.0; float rr = mix(3.5, 13.0, hash11(fi * 5.23 + 9.0)); float d = length(p - c); float aa = max(fp, 0.6); m += smoothstep(rr + aa, rr - aa, d) * 0.55; rim += smoothstep(rr + aa, rr - aa, d) * smoothstep(rr - 3.4 - aa, rr - 1.2, d); } return min(m, 1.0); } // polka dots on a jittered grid float polka(vec2 p, float cell, float rad, float fp){ vec2 gi = floor(p / cell); vec2 gf = fract(p / cell) - 0.5; vec2 j = (vec2(hash12(gi), hash12(gi + 5.3)) - 0.5) * 0.45; return smoothstep(rad + fp / cell, rad * 0.72, length(gf - j)); } // evenly spaced lines, for grids float lineAt(float x, float period, float w, float fp){ return smoothstep(w + fp, max(w - fp, 0.0), abs(mod(x, period) - period * 0.5)); } // Type set on an arc: letters are radial dashes of uneven height, // evenly spaced in ARC LENGTH so the pitch stays constant. float arcText(float r, float a, float rc, float h, float a0, float span, float pitch, float seed, float fp){ float da = wrapPi(a - a0); float win = smoothstep(span, span * 0.86, abs(da)); if (win < 0.002) return 0.0; float sl = da * rc; float gi = floor(sl / pitch); float g = fract(sl / pitch); float hh = h * mix(0.60, 1.0, hash11(gi * 1.7 + seed)); return win * smoothstep(hh + fp, hh - 0.35, abs(r - rc)) * smoothstep(0.76, 0.60, g) * smoothstep(0.03, 0.17, g); } // A marker scrawl, same idea: a fat wobbling stroke that follows // the annulus and breaks into runs of ink. Handwriting moves along // its line far more than across it, so the wobble stays low. float arcMarker(float r, float a, float rc, float a0, float span, float fp){ float da = wrapPi(a - a0); float win = smoothstep(span, span * 0.90, abs(da)); if (win < 0.002) return 0.0; float sl = da * rc; float wob = 2.5 * sin(sl * 0.21) + 1.3 * sin(sl * 0.46 + 1.1) + 0.45 * sin(sl * 0.88 + 2.2); float run = smoothstep(0.13, 0.26, fract(sl * 0.030 + 0.2)); return win * run * smoothstep(2.1 + fp, 0.8, abs(r - rc - wob)); } // A track list. The lines crossing the middle break in two on // their own, because the clamping area carries no ink -- what // happens when you write across a disc with a hole in it. Straight // here, where arcs were right for a wordmark: a list IS a // left-aligned block. float mixtape(vec2 p, float fp, out float blue){ float m = 0.0; blue = 0.0; for (int i = 0; i < 8; i++){ float fi = float(i); float title = (fi < 0.5) ? 1.0 : 0.0; float ph = fi * 1.37; float x0 = mix(-46.0, -34.0, title); // Track names run most of the way across. The hub cuts the middle // rows in two, which is the point. float len = mix(mix(36.0, 84.0, hash11(fi * 3.7 + 2.0)), 56.0, title); float x = p.x - x0; // Nobody writes level. A per-line slope is the single strongest cue // that a row of marks is handwriting rather than a row of marks. float slope = (hash11(fi * 8.3 + 4.0) - 0.5) * 0.13; float base = 41.0 - fi * 11.0 + slope * x + 0.75 * sin(x * 0.26 + ph) + 0.35 * sin(x * 0.55 + ph * 2.1); float d = p.y - base; // Letters ~2.7mm apart, each with its own pen pressure. float gi = floor(x / 2.7); float g = fract(x / 2.7); float hA = hash11(gi * 1.7 + ph); float hB = hash11(gi * 3.9 + ph * 1.3); float w = mix(1.15, 1.75, hA) * mix(1.0, 1.75, title); float run = smoothstep(0.20, 0.36, fract(x * 0.055 + fi * 0.41)); float span = smoothstep(len, len - 4.0, x) * smoothstep(-3.0, 1.0, x); float line = smoothstep(w + fp, w * 0.35, abs(d)) * run * span; // Ascenders and descenders: what stop a wobbling line // reading as a wobbling line. float tick = smoothstep(0.80 + fp, 0.25, abs(g - 0.5) * 2.7) * run * span; line = max(line, tick * step(0.68, hA) * smoothstep(3.8, 0.0, d) * step(0.0, d)); line = max(line, tick * step(0.84, hB) * smoothstep(-2.9, 0.0, d) * step(d, 0.0)); // the number somebody always puts in front of a track line = max(line, (1.0 - title) * smoothstep(1.1 + fp, 0.35, abs(x - 2.0)) * smoothstep(2.0 + fp, 0.7, abs(d))); // one pen ran out and got swapped for a biro if (fi > 0.5 && hash11(fi * 5.9 + 11.0) > 0.68) blue = max(blue, line); else m = max(m, line); } // the title gets underlined, because it always does m = max(m, smoothstep(1.3 + fp, 0.4, abs(p.y - 35.4 - 0.8 * sin(p.x * 0.12))) * smoothstep(-36.0, -32.0, p.x) * smoothstep(22.0, 16.0, p.x)); return m; } // the spec band every recordable disc carries vec3 specBand(vec3 c, float r, float a, vec3 ink, float fp){ c = mix(c, ink, dashRing(r, a, 25.5, 1.0, 190.0, 0.5, fp) * 0.90); c = mix(c, ink, dashRing(r, a, 55.5, 0.8, 260.0, 0.5, fp) * 0.75); return mix(c, ink, smoothstep(0.75 + fp, 0.25, abs(r - 52.5)) * 0.85); } // Returns the printed albedo; `gloss` comes back as how shiny the // lacquer is there, because ink is flatter than bare stock. vec3 labelArt(vec2 lp, float r, float a, float fp, out float gloss){ float d = gLabel; float aa = max(fp, 0.5); float paper = 0.80 + 0.16 * vnoise(lp * 1.7); vec3 c = vec3(paper); gloss = 0.62; if (d < 0.5){ // ---- 0: inkjet Y2K. Diagonal wash, swooshes, bubbles. ---- float diag = smoothstep(-46.0, 16.0, lp.x * 0.70 + lp.y * 0.71); vec3 wash = mix(vec3(0.05, 0.66, 0.95), vec3(0.58, 0.08, 0.74), 0.5 + 0.5 * cos(a - 0.35)); wash = mix(wash, vec3(0.03, 0.12, 0.60), smoothstep(20.0, 58.0, r) * 0.60); c = mix(c, wash, diag * 0.96); c = mix(c, vec3(0.06, 0.84, 0.99), crescent(lp, vec2(7.0, 1.0), 47.0, vec2(-9.0, -25.0), 51.0, aa) * 0.85); c = mix(c, vec3(0.98, 1.00, 1.00), crescent(lp, vec2(-2.0, -12.0), 57.0, vec2(-28.0, -50.0), 62.0, aa) * 0.88); vec3 warm = mix(vec3(0.99, 0.52, 0.07), vec3(0.90, 0.11, 0.46), smoothstep(18.0, 56.0, r)); c = mix(c, warm, crescent(lp, vec2(16.0, 26.0), 48.0, vec2(34.0, 54.0), 50.0, aa) * 0.90); vec2 bp = lp - vec2(-24.0, 21.0); float bd = length(bp); c = mix(c, vec3(1.0), clamp((pow(max(sin(atan(bp.y, bp.x) * 9.0), 0.0), 7.0) * exp(-bd * 0.055) + exp(-bd * bd * 0.012)) * 0.9, 0.0, 1.0)); vec2 hc = fract(lp * 0.30) - 0.5; c = mix(c, vec3(0.05, 0.10, 0.30), smoothstep(0.36, 0.16, length(hc)) * smoothstep(0.30, 0.85, cos(a + 1.15)) * smoothstep(24.0, 44.0, r) * 0.42); float rim; float bm = bubbles(lp, 0.0, fp, rim); c = mix(c, c * 1.30 + vec3(0.07, 0.13, 0.18), bm * 0.62); c = mix(c, vec3(1.0), clamp(rim, 0.0, 1.0) * 0.85); c = specBand(c, r, a, vec3(0.05, 0.07, 0.16), fp); c = mix(c, vec3(0.03, 0.05, 0.13), arcText(r, a, 41.5, 3.1, -1.05, 0.60, 3.1, 4.0, fp) * 0.95); c = mix(c, vec3(0.10, 0.15, 0.34), arcText(r, a, 33.5, 1.15, -1.05, 0.52, 1.5, 11.0, fp) * 0.85); gloss = 0.74; return c; } if (d < 1.5){ // ---- 1: silver stock, written on with a marker ---- c = vec3(0.76, 0.76, 0.73) * (0.88 + 0.18 * vnoise(lp * 2.3)); c = mix(c, vec3(0.10, 0.13, 0.30), smoothstep(1.2 + fp, 0.4, abs(r - 27.0)) * 0.9); c = mix(c, vec3(0.10, 0.13, 0.30), dashRing(r, a, 24.0, 0.9, 150.0, 0.5, fp) * 0.8); c = mix(c, vec3(0.16, 0.18, 0.22), dashRing(r, a, 55.0, 0.8, 210.0, 0.5, fp) * 0.7); float ink = max(arcMarker(r, a, 41.0, 1.85, 1.15, fp), arcMarker(r, a, 31.0, 1.72, 0.95, fp)); ink = max(ink, smoothstep(1.6 + fp, 0.5, abs(r - 25.5)) * smoothstep(0.62, 0.54, abs(wrapPi(a - 1.80)))); c = mix(c, vec3(0.045, 0.040, 0.055), ink); gloss = mix(0.62, 0.10, ink); return c; } if (d < 2.5){ // ---- 2: Frutiger Aero. Glass, water, one leaf's worth of green. ---- vec3 sky = mix(vec3(0.06, 0.55, 0.86), vec3(0.72, 0.93, 1.00), smoothstep(-40.0, 30.0, lp.y)); vec3 grass = mix(vec3(0.32, 0.74, 0.16), vec3(0.08, 0.42, 0.14), smoothstep(-10.0, -52.0, lp.y)); c = mix(sky, grass, smoothstep(-4.0, -14.0, lp.y)); // the glossy sweep across the top, the thing that made it Aero c = mix(c, vec3(1.0), crescent(lp, vec2(0.0, 8.0), 56.0, vec2(-4.0, -46.0), 70.0, aa) * 0.42); float rim; float bm = bubbles(lp, 3.7, fp, rim); c = mix(c, c * 1.35 + vec3(0.10, 0.14, 0.16), bm * 0.55); c = mix(c, vec3(1.0), clamp(rim, 0.0, 1.0) * 0.90); c = specBand(c, r, a, vec3(0.02, 0.14, 0.22), fp); c = mix(c, vec3(1.0), arcText(r, a, 41.5, 3.0, 1.90, 0.62, 3.0, 21.0, fp) * 0.92); gloss = 0.88; return c; } if (d < 3.5){ // ---- 3: neon grid. Dark plastic, magenta wireframe, glow. ---- c = mix(vec3(0.020, 0.024, 0.075), vec3(0.06, 0.02, 0.13), smoothstep(20.0, 58.0, r)); float rings = lineAt(r, 6.5, 0.30, fp); float spokes = lineAt(a * r, 6.5 * r / max(r, 8.0) * 3.0, 0.34, fp); vec3 neon = mix(vec3(0.95, 0.10, 0.62), vec3(0.10, 0.85, 0.98), smoothstep(22.0, 56.0, r)); c += neon * (rings * 0.85 + spokes * 0.55); c += neon * 0.16 * exp(-abs(mod(r, 6.5) - 3.25) * 0.9); c = mix(c, vec3(0.95, 0.98, 1.0), smoothstep(1.4 + fp, 0.5, abs(r - 30.0)) * 0.85); c = specBand(c, r, a, vec3(0.55, 0.85, 1.0), fp); c = mix(c, vec3(0.10, 0.98, 0.85), arcText(r, a, 41.0, 2.9, -2.30, 0.58, 3.0, 33.0, fp) * 0.95); gloss = 0.80; return c; } if (d < 4.5){ // ---- 4: flames. Tongues off the hub, tribal and orange. ---- c = vec3(0.030, 0.028, 0.032) * (0.8 + 0.4 * vnoise(lp * 1.3)); float k = 0.5 + 0.5 * sin(a * 7.0 + 1.35 * sin(a * 3.0 + 0.7)); float k2 = 0.5 + 0.5 * sin(a * 11.0 - 2.1 * sin(a * 2.0)); float top = 24.0 + 30.0 * k; float top2 = 22.0 + 20.0 * k2; float f1 = smoothstep(top + fp, top - 8.0, r); float f2 = smoothstep(top2 + fp, top2 - 5.0, r); vec3 hot = mix(vec3(1.00, 0.92, 0.35), vec3(0.94, 0.16, 0.03), smoothstep(20.0, top, r)); c = mix(c, hot * 0.55, f1); c = mix(c, hot, f2); c = mix(c, vec3(1.00, 0.96, 0.70), f2 * smoothstep(top2 - 5.0, top2 - 12.0, r) * 0.55); c = specBand(c, r, a, vec3(0.95, 0.70, 0.30), fp); c = mix(c, vec3(1.0, 0.88, 0.55), arcText(r, a, 44.5, 2.4, 1.35, 0.50, 2.8, 51.0, fp) * 0.9); gloss = 0.55; return c; } if (d < 5.5){ // ---- 5: bubblegum. Starburst, dots, a badge. ---- c = mix(vec3(1.00, 0.36, 0.66), vec3(1.00, 0.76, 0.20), smoothstep(18.0, 58.0, r)); float rays = pow(max(sin(a * 8.0 + 0.4), 0.0), 3.0); c = mix(c, vec3(1.00, 0.93, 0.30), rays * 0.55 * smoothstep(20.0, 40.0, r)); c = mix(c, vec3(1.0), polka(lp, 9.5, 0.20, fp) * 0.80); c = mix(c, vec3(0.16, 0.06, 0.30), smoothstep(15.0 + fp, 14.2, abs(length(lp - vec2(0.0, 34.0)))) * 0.0); float badge = smoothstep(11.0 + fp, 10.2, length(lp - vec2(-4.0, 33.0))); c = mix(c, vec3(0.20, 0.07, 0.34), badge); c = mix(c, vec3(1.00, 0.93, 0.30), badge * smoothstep(8.4, 7.6, length(lp - vec2(-4.0, 33.0))) * 0.0); c = specBand(c, r, a, vec3(0.30, 0.05, 0.24), fp); c = mix(c, vec3(0.22, 0.04, 0.26), arcText(r, a, 43.0, 2.6, -1.55, 0.55, 2.9, 67.0, fp) * 0.92); gloss = 0.70; return c; } if (d < 6.5){ // ---- 6: bootleg. Inkjet stripe over plain stock. ---- c = vec3(0.84, 0.84, 0.82) * (0.90 + 0.14 * vnoise(lp * 2.1)); float band = smoothstep(13.0 + fp, 11.5, abs(lp.y * 0.94 - lp.x * 0.34 - 6.0)); vec3 stripe = mix(vec3(0.10, 0.24, 0.72), vec3(0.72, 0.14, 0.34), smoothstep(-40.0, 40.0, lp.x)); c = mix(c, stripe, band); c = mix(c, vec3(1.0), band * smoothstep(3.0 + fp, 1.6, abs(lp.y * 0.94 - lp.x * 0.34 - 15.0)) * 0.75); c = specBand(c, r, a, vec3(0.10, 0.12, 0.20), fp); c = mix(c, vec3(0.06, 0.08, 0.14), arcText(r, a, 41.0, 2.7, 2.55, 0.56, 3.0, 83.0, fp) * 0.9); c = mix(c, vec3(0.10, 0.14, 0.26), arcText(r, a, 32.5, 1.1, 2.55, 0.48, 1.5, 91.0, fp) * 0.8); gloss = 0.66; return c; } if (d < 7.5){ // ---- 7: camo. Four tones thresholded off two noise fields. ---- float n1 = vnoise(lp * 0.055) * 0.66 + vnoise(lp * 0.135) * 0.34; float n2 = vnoise(lp * 0.070 + 40.0) * 0.72 + vnoise(lp * 0.17 + 9.0) * 0.28; c = vec3(0.31, 0.32, 0.20); c = mix(c, vec3(0.50, 0.44, 0.27), smoothstep(0.47, 0.51, n1)); c = mix(c, vec3(0.15, 0.18, 0.12), smoothstep(0.55, 0.59, n2)); c = mix(c, vec3(0.07, 0.07, 0.06), smoothstep(0.52, 0.56, n1 * n2 * 1.9)); c = specBand(c, r, a, vec3(0.86, 0.87, 0.78), fp); c = mix(c, vec3(0.90, 0.92, 0.80), arcText(r, a, 42.0, 2.6, 0.65, 0.52, 2.9, 101.0, fp) * 0.9); gloss = 0.42; return c; } if (d < 8.5){ // ---- 8: chrome. A gradient with a hard horizon cut through it, // which is the whole trick: chrome is a picture of a room. ---- float g1 = 0.5 + 0.5 * sin(lp.y * 0.095 + lp.x * 0.042); c = mix(vec3(0.26, 0.28, 0.34), vec3(0.93, 0.95, 1.00), smoothstep(0.20, 0.80, g1)); float hz = lp.y + lp.x * 0.42 + 6.0; c = mix(c, vec3(0.05, 0.07, 0.13), smoothstep(2.6 + fp, 0.9, abs(hz))); c = mix(c, vec3(0.62, 0.78, 0.98), smoothstep(0.0, -16.0, hz) * 0.45); c = mix(c, vec3(1.0), crescent(lp, vec2(-6.0, 6.0), 52.0, vec2(-26.0, -34.0), 58.0, aa) * 0.55); c = specBand(c, r, a, vec3(0.05, 0.07, 0.14), fp); c = mix(c, vec3(0.04, 0.06, 0.12), arcText(r, a, 42.5, 3.0, -0.40, 0.56, 3.1, 113.0, fp) * 0.92); gloss = 0.92; return c; } if (d < 9.5){ // ---- 9: tribal. Pointed tongues off a phase-modulated sine, // which is every tattoo flash sheet of 2002. ---- c = vec3(0.040, 0.038, 0.044); float k = sin(a * 5.0 + 2.4 * sin(a * 2.0 + r * 0.045)); float t1 = smoothstep(0.94, 0.998, abs(k)) * smoothstep(23.0, 29.0, r) * smoothstep(58.0, 48.0, r); float k2 = sin(a * 3.0 - 1.7 * sin(a + r * 0.08) + 1.6); float t2 = smoothstep(0.965, 0.999, abs(k2)) * smoothstep(25.0, 33.0, r); c = mix(c, vec3(0.94, 0.95, 0.93), max(t1, t2 * 0.8)); c = specBand(c, r, a, vec3(0.80, 0.82, 0.80), fp); gloss = 0.60; return c; } if (d < 10.5){ // ---- 10: falling code. Columns of glyphs, brightest at the head. ---- c = vec3(0.006, 0.026, 0.013); float cl = floor(lp.x / 4.7); float ph = hash11(cl * 1.73 + 0.4); float yy = lp.y / 5.4 + ph * 37.0; float gl = step(0.30, hash12(vec2(cl, floor(yy)))); float head = fract(yy * 0.075 + ph); // the trail dims fast behind the head, which is the only thing // that separates a column of code from a field of green blocks c += vec3(0.10, 0.90, 0.28) * gl * pow(smoothstep(0.98, 0.10, head), 2.2) * 0.95; c += vec3(0.80, 1.00, 0.88) * gl * smoothstep(0.965, 0.995, head) * 1.15; c = specBand(c, r, a, vec3(0.30, 1.00, 0.45), fp); gloss = 0.78; return c; } if (d < 11.5){ // ---- 11: chequered flag, on a band ---- c = vec3(0.88, 0.88, 0.86) * (0.92 + 0.12 * vnoise(lp * 2.0)); float band = smoothstep(30.0 - fp, 31.5, r) * smoothstep(46.0 + fp, 44.5, r); float chk = mod(floor(a * 18.0 / TAU) + floor(r / 7.5), 2.0); c = mix(c, mix(vec3(0.06), vec3(0.97), chk), band); c = mix(c, vec3(0.86, 0.10, 0.10), smoothstep(1.6 + fp, 0.6, abs(r - 28.0))); c = mix(c, vec3(0.86, 0.10, 0.10), smoothstep(1.6 + fp, 0.6, abs(r - 48.5))); c = specBand(c, r, a, vec3(0.08, 0.09, 0.12), fp); c = mix(c, vec3(0.06, 0.07, 0.10), arcText(r, a, 53.0, 1.9, 2.05, 0.48, 2.6, 127.0, fp) * 0.9); gloss = 0.68; return c; } if (d < 12.5){ // ---- 12: splatter. Thresholded noise, which is what a splat is. ---- c = vec3(0.87, 0.87, 0.85) * (0.92 + 0.12 * vnoise(lp * 2.2)); float g1 = vnoise(lp * 0.115) * 0.68 + vnoise(lp * 0.30) * 0.32; c = mix(c, vec3(0.055, 0.05, 0.06), smoothstep(0.60, 0.645, g1)); float g2 = vnoise(lp * 0.16 + 17.0) * 0.70 + vnoise(lp * 0.44 + 3.0) * 0.30; c = mix(c, vec3(0.84, 0.11, 0.09), smoothstep(0.665, 0.70, g2)); float g3 = vnoise(lp * 0.62 + 31.0); c = mix(c, vec3(0.055, 0.05, 0.06), smoothstep(0.74, 0.80, g3) * 0.7); c = specBand(c, r, a, vec3(0.08, 0.08, 0.10), fp); c = mix(c, vec3(0.05, 0.05, 0.07), arcText(r, a, 41.0, 3.0, -2.7, 0.54, 3.0, 139.0, fp) * 0.92); gloss = 0.52; return c; } if (d < 13.5){ // ---- 13: acid smiley. The face is the annulus -- a CD already // has a hole where a nose would go, so it draws itself. ---- c = vec3(1.00, 0.85, 0.06); float eye = min(length(lp - vec2(-19.0, 25.0)), length(lp - vec2(19.0, 25.0))); c = mix(c, vec3(0.05, 0.04, 0.03), smoothstep(6.6 + fp, 5.0, eye)); vec2 sc = lp - vec2(0.0, 7.0); float sl = max(length(sc), 1e-3); float smile = smoothstep(3.4 + fp, 2.0, abs(sl - 33.0)) * smoothstep(-0.12, -0.42, sc.y / sl); c = mix(c, vec3(0.05, 0.04, 0.03), smile); c = specBand(c, r, a, vec3(0.09, 0.07, 0.03), fp); gloss = 0.82; return c; } if (d < 14.5){ // ---- 14: acid. A phase-modulated polar sine, which is the // cheapest honest way to get a 303 line drawn as a picture. ---- float w = a * 3.0 + r * 0.30 + 2.3 * sin(r * 0.105 + a * 2.0); float b1 = 0.5 + 0.5 * sin(w); float b2 = 0.5 + 0.5 * sin(w * 2.0 + 1.1); c = mix(vec3(0.03, 0.05, 0.02), vec3(0.72, 1.00, 0.06), smoothstep(0.32, 0.78, b1)); c = mix(c, vec3(0.95, 0.05, 0.62), smoothstep(0.68, 0.93, b2) * 0.85); c = mix(c, vec3(0.02, 0.04, 0.02), smoothstep(0.88, 0.99, b1 * b2)); c = specBand(c, r, a, vec3(0.85, 1.00, 0.30), fp); gloss = 0.86; return c; } if (d < 15.5){ // ---- 15: hazard. Diagonal stripes and a warning ring. ---- float st = mod(floor((lp.x + lp.y) * 0.7071 / 7.5), 2.0); c = mix(vec3(0.05, 0.05, 0.04), vec3(1.00, 0.82, 0.03), st); c = mix(c, vec3(0.05, 0.05, 0.04), smoothstep(3.0 + fp, 1.6, abs(r - 33.0))); c = mix(c, vec3(1.00, 0.82, 0.03), smoothstep(1.3 + fp, 0.4, abs(r - 33.0))); c = specBand(c, r, a, vec3(0.06, 0.06, 0.05), fp); c = mix(c, vec3(0.05, 0.05, 0.04), arcText(r, a, 43.0, 2.6, 1.20, 0.52, 2.9, 151.0, fp) * 0.92); gloss = 0.62; return c; } if (d < 16.5){ // ---- 16: op-art spiral. Three arms, one turn per 18mm, so it // stays coarse enough to read when the disc is small on screen. ---- float sp = fract(a * 3.0 / TAU + r * 0.055); c = mix(vec3(0.035, 0.035, 0.040), vec3(0.96, 0.96, 0.94), smoothstep(0.47, 0.53, sp) * smoothstep(0.97, 0.91, sp)); c = mix(c, vec3(0.90, 0.09, 0.28), smoothstep(1.6 + fp, 0.5, abs(r - 51.0))); c = specBand(c, r, a, vec3(0.90, 0.09, 0.28), fp); gloss = 0.74; return c; } if (d < 17.5){ // ---- 17: a mixtape. Title, underline, numbered tracks. ---- c = vec3(0.81, 0.81, 0.78) * (0.90 + 0.16 * vnoise(lp * 2.2)); c = mix(c, vec3(0.16, 0.18, 0.34), dashRing(r, a, 55.5, 0.7, 220.0, 0.5, fp) * 0.45); float blue; float ink = mixtape(lp, fp, blue); c = mix(c, vec3(0.05, 0.05, 0.06), ink); c = mix(c, vec3(0.10, 0.16, 0.52), blue); gloss = mix(0.60, 0.10, max(ink, blue)); return c; } if (d < 18.5){ // ---- 18: black metal. An illegible spiky logo, which is the // genre's whole typographic project, plus a hairline ring. ---- c = vec3(0.028, 0.026, 0.030) * (0.80 + 0.40 * vnoise(lp * 1.6)); float spike = abs(sin(a * 17.0 + 1.15 * sin(a * 6.0 + 0.7))); float top = 31.0 + 21.0 * pow(spike, 3.0); float lg = smoothstep(top + fp, top - 2.4, r) * smoothstep(26.5, 29.0, r); c = mix(c, vec3(0.93, 0.94, 0.92), lg); c = mix(c, vec3(0.88, 0.89, 0.88), smoothstep(0.7 + fp, 0.2, abs(r - 55.5)) + smoothstep(0.6 + fp, 0.2, abs(r - 24.0))); c = specBand(c, r, a, vec3(0.72, 0.73, 0.72), fp); gloss = 0.30; return c; } if (d < 19.5){ // ---- 19: punk. Photocopied to death, with ransom-note blocks // pasted on at whatever angle they came off the scissors. ---- c = vec3(0.91, 0.91, 0.89) * (0.84 + 0.30 * vnoise(lp * 3.0)); float grit = vnoise(lp * 0.85) * 0.6 + vnoise(lp * 2.3) * 0.4; c = mix(c, vec3(0.055, 0.05, 0.055), smoothstep(0.690, 0.735, grit)); for (int i = 0; i < 7; i++){ float fi = float(i); vec2 bc = vec2(hash11(fi * 4.1 + 1.0), hash11(fi * 6.3 + 8.0)) * 84.0 - 42.0; float ba = (hash11(fi * 2.9 + 5.0) - 0.5) * 1.1; float cs = cos(ba), sn = sin(ba); vec2 q = lp - bc; q = vec2(q.x * cs + q.y * sn, -q.x * sn + q.y * cs); vec2 hs = vec2(mix(5.0, 10.0, hash11(fi * 7.7 + 3.0)), 6.5); float blk = smoothstep(hs.x + fp, hs.x - 0.7, abs(q.x)) * smoothstep(hs.y + fp, hs.y - 0.7, abs(q.y)); float inv = step(0.5, hash11(fi * 9.1 + 2.0)); c = mix(c, mix(vec3(0.055, 0.05, 0.055), vec3(0.95, 0.95, 0.93), inv), blk); c = mix(c, mix(vec3(0.95, 0.95, 0.93), vec3(0.055, 0.05, 0.055), inv), blk * smoothstep(2.4 + fp, 1.4, abs(q.y)) * 0.95); } // the circle somebody drew round it by hand c = mix(c, vec3(0.055, 0.05, 0.055), smoothstep(2.1 + fp, 0.7, abs(r - 40.0 - 2.2 * sin(a * 3.0 + 1.0)))); c = specBand(c, r, a, vec3(0.07, 0.06, 0.07), fp); gloss = 0.30; return c; } if (d < 20.5){ // ---- 20: hardcore. Two colours, a stencil bar, distressed. ---- c = vec3(0.055, 0.050, 0.052); float bar = smoothstep(11.5 + fp, 9.2, abs(lp.y * 0.86 - lp.x * 0.51 + 5.0)); c = mix(c, vec3(0.82, 0.09, 0.07), bar); float burst = smoothstep(0.0, -2.5, r - (17.0 + 15.0 * cos(a * 5.0 - 1.2))); c = mix(c, vec3(0.95, 0.93, 0.88), burst * 0.92); // the stencil never sprays clean float wear = vnoise(lp * 1.15) * 0.65 + vnoise(lp * 3.1) * 0.35; c = mix(c, vec3(0.055, 0.050, 0.052), smoothstep(0.54, 0.63, wear) * 0.7); c = specBand(c, r, a, vec3(0.90, 0.88, 0.84), fp); c = mix(c, vec3(0.95, 0.93, 0.88), arcText(r, a, 44.0, 2.8, -0.9, 0.54, 3.0, 163.0, fp) * 0.9); gloss = 0.36; return c; } // ---- 21: blank stock ---- c = vec3(0.74, 0.74, 0.71) * (0.88 + 0.18 * vnoise(lp * 2.3)); c = mix(c, vec3(0.30, 0.32, 0.36), dashRing(r, a, 55.0, 0.7, 210.0, 0.5, fp) * 0.5); gloss = 0.58; return c; } vec3 shadeLabel(vec3 p, vec3 rd, vec3 lp3, float fp){ vec3 N = vec3(0.0, -1.0, 0.0); vec2 lp = p.xz; float r = length(lp); float a = atan(lp.y, lp.x); vec3 dl = lp3 - p; float d2 = dot(dl, dl); vec3 L = dl * inversesqrt(d2); float irr = uLampPower * 30000.0 / max(d2, 1.0); float ndl = max(dot(N, L), 0.0); // Print starts outside the clamping area and stops short of the // edge; inside and outside that is bare polycarbonate. float ink = smoothstep(R_CLAMP - 1.2, R_CLAMP + 1.4, r) * smoothstep(R_OUT - 0.5, R_OUT - 2.4, r); float gloss; vec3 art = labelArt(lp, r, a, fp, gloss); vec3 base = mix(vec3(0.030, 0.030, 0.036), art, ink); float g = mix(0.12, gloss, ink); return base * (ndl * irr + uAmbient * 1.8) + vec3(0.95, 0.95, 1.0) * ggx(L, -rd, N, mix(0.44, 0.16, g)) * ndl * irr * 0.09 + studioEnv(gM * reflect(rd, N)) * mix(0.34, 0.05 + 0.20 * g, ink); } // ---- the floppies ---- // A 3.5 inch disk is 90 x 94 x 3.3 mm with a corner cut off: a // box and a half-space, both closed form. That cut corner is the // only part of the silhouette that says floppy not coaster. // // It tumbles on the same law as the discs, which is not a // shortcut. The perpendicular axis theorem gives a flat plate // I3 = I1 + I2, so a SQUARE plate has I3 = 2*I1 exactly -- the // same ratio as a disc, hence the same psi = -phi*cos(theta)/2. // A floppy is 90 by 94, within four percent of square. #define F_W 45.0 // half width #define F_T 1.65 // half thickness #define F_H 47.0 // half height // a filled rectangle, 1 inside, antialiased by the footprint float bx2(vec2 p, vec2 b, float fp){ vec2 d = abs(p) - b; return smoothstep(fp, -fp, max(d.x, d.y)); } vec3 shellColour(float k){ if (k < 0.5) return vec3(0.042, 0.042, 0.047); // black, the default if (k < 1.5) return vec3(0.60, 0.575, 0.50); // ivory, the older ones if (k < 2.5) return vec3(0.075, 0.19, 0.52); if (k < 3.5) return vec3(0.50, 0.075, 0.085); if (k < 4.5) return vec3(0.70, 0.58, 0.055); if (k < 5.5) return vec3(0.075, 0.40, 0.19); return vec3(0.19, 0.20, 0.235); // smoke } // A row of block letters. Period lettering, not anybody's // trademark: every mark on these labels is a style of the era, not // a copy of a real logo, which is not a thing to publish into the // public domain on someone else's behalf. float wordBar(vec2 q, vec2 ctr, vec2 hf, float pitch, float seed, float fp){ vec2 d = q - ctr; float gi = floor((d.x + hf.x) / pitch); float g = fract((d.x + hf.x) / pitch); float hh = hf.y * mix(0.55, 1.0, hash11(gi * 1.7 + seed)); // `half` is a reserved word in GLSL ES and this was named that return smoothstep(fp, -fp, abs(d.x) - hf.x) * smoothstep(0.70, 0.55, g) * smoothstep(0.05, 0.20, g) * smoothstep(hh + fp, hh - 0.3, abs(d.y)); } // The paper label. Nine of them, because a box of floppies never // had two the same. vec3 floppyLabel(vec2 q, float fp, out float ink){ ink = 0.0; vec3 c = vec3(0.88, 0.87, 0.83) * (0.93 + 0.10 * vnoise(q * 2.4)); float v = gLabel; if (v < 1.5){ // 0/1: blank, or written on if (v > 0.5){ float blue; float m = mixtape(q * 1.55, fp * 1.55, blue); c = mix(c, vec3(0.06, 0.06, 0.07), m); c = mix(c, vec3(0.10, 0.16, 0.52), blue); ink = max(m, blue); } return c; } if (v < 2.5){ // 2: a printed brand band across the top, with microtext c = mix(c, vec3(0.09, 0.24, 0.62), bx2(q - vec2(0.0, 21.0), vec2(33.0, 7.0), fp)); c = mix(c, vec3(0.93, 0.94, 0.96), bx2(q - vec2(-8.0, 21.0), vec2(15.0, 2.0), fp) * smoothstep(0.55, 0.40, fract(q.x * 0.62))); for (int i = 0; i < 4; i++){ float fy = 8.0 - float(i) * 8.0; c = mix(c, vec3(0.35, 0.36, 0.40), bx2(q - vec2(-4.0, fy), vec2(26.0, 0.7), fp) * smoothstep(0.60, 0.42, fract(q.x * 0.9 + float(i)))); } return c; } if (v < 3.5){ // 3: colour coded -- a big dot and a couple of lines c = mix(c, vec3(0.86, 0.30, 0.06), smoothstep(fp, -fp, length(q - vec2(-20.0, 16.0)) - 8.0)); float blue; float m = mixtape(q * 1.9 + vec2(14.0, -8.0), fp * 1.9, blue); c = mix(c, vec3(0.07, 0.07, 0.08), m); ink = m; return c; } if (v < 4.5){ // 4: the six-band stripe. Nothing said 1984 louder. float band = bx2(q - vec2(0.0, 14.0), vec2(33.0, 10.8), fp); float bi = clamp(floor((24.8 - q.y) / 3.6), 0.0, 5.0); vec3 bc = (bi < 0.5) ? vec3(0.36, 0.70, 0.24) : (bi < 1.5) ? vec3(0.97, 0.81, 0.06) : (bi < 2.5) ? vec3(0.94, 0.53, 0.05) : (bi < 3.5) ? vec3(0.85, 0.10, 0.12) : (bi < 4.5) ? vec3(0.49, 0.14, 0.55) : vec3(0.10, 0.47, 0.79); c = mix(c, bc, band); c = mix(c, vec3(0.10, 0.11, 0.14), wordBar(q, vec2(-4.0, -6.0), vec2(24.0, 4.2), 3.4, 7.0, fp)); c = mix(c, vec3(0.35, 0.36, 0.40), wordBar(q, vec2(-10.0, -17.0), vec2(17.0, 1.5), 1.9, 21.0, fp)); return c; } if (v < 5.5){ // 5: a board. Solder mask, gold traces, pads, a hex badge. c = vec3(0.055, 0.29, 0.185); float tr = max(lineAt(q.y - 4.0, 9.0, 0.55, fp), lineAt(q.x + 3.0, 11.0, 0.55, fp) * step(q.y, 6.0)); c = mix(c, vec3(0.82, 0.66, 0.20), tr * 0.95); vec2 pd = mod(q + vec2(1.5, 0.5), vec2(11.0, 9.0)) - vec2(5.5, 4.5); c = mix(c, vec3(0.86, 0.70, 0.24), smoothstep(fp, -fp, length(pd) - 1.6)); c = mix(c, vec3(0.055, 0.29, 0.185), smoothstep(fp, -fp, length(pd) - 0.7)); // the hex badge every board silkscreen had vec2 hq = abs(q - vec2(19.0, 17.0)); float hex = max(hq.x * 0.866 + hq.y * 0.5, hq.y) - 7.0; c = mix(c, vec3(0.90, 0.92, 0.88), smoothstep(fp, -fp, hex) * smoothstep(-1.6, -2.2, hex)); c = mix(c, vec3(0.88, 0.90, 0.86), wordBar(q, vec2(-9.0, -20.0), vec2(20.0, 2.6), 2.4, 33.0, fp)); return c; } if (v < 6.5){ // 6: graph paper, written on c = mix(c, vec3(0.60, 0.71, 0.86), max(lineAt(q.x, 4.2, 0.16, fp), lineAt(q.y, 4.2, 0.16, fp)) * 0.75); c = mix(c, vec3(0.30, 0.44, 0.70), max(lineAt(q.x, 21.0, 0.26, fp), lineAt(q.y, 21.0, 0.26, fp)) * 0.7); float blue; float m = mixtape(q * 1.55 + vec2(0.0, 6.0), fp * 1.55, blue); c = mix(c, vec3(0.06, 0.06, 0.07), m); c = mix(c, vec3(0.10, 0.16, 0.52), blue); ink = max(m, blue); return c; } if (v < 7.5){ // 7: corporate speed stripes and a bevelled wordmark float st = q.y * 0.94 - q.x * 0.34; c = mix(c, vec3(0.07, 0.21, 0.58), smoothstep(4.2 + fp, 2.6, abs(st - 15.0))); c = mix(c, vec3(0.80, 0.11, 0.10), smoothstep(2.7 + fp, 1.5, abs(st - 8.5))); c = mix(c, vec3(0.07, 0.21, 0.58), smoothstep(1.5 + fp, 0.7, abs(st - 3.5))); c = mix(c, vec3(0.13, 0.14, 0.18), wordBar(q, vec2(-6.0, -8.0), vec2(25.0, 4.6), 3.6, 51.0, fp)); c = mix(c, vec3(0.72, 0.76, 0.84), wordBar(q, vec2(-6.0, -6.6), vec2(25.0, 3.0), 3.6, 51.0, fp) * 0.8); c = mix(c, vec3(0.40, 0.42, 0.48), wordBar(q, vec2(-13.0, -19.0), vec2(16.0, 1.4), 1.8, 67.0, fp)); return c; } // 8: no label at all, bare shell ink = -1.0; return c; } // n is the outward face normal in the floppy's own frame. vec3 shadeFloppy(vec3 p, vec3 n, vec3 rd, vec3 lp3, float fp){ vec3 shell = shellColour(gDye); vec3 c = shell * (0.92 + 0.16 * vnoise(p.xz * 1.9)); // moulding texture float rough = 0.42; if (abs(n.y) > 0.5){ vec2 q = vec2(p.x, p.z) * vec2(sign(n.y), 1.0); // mirror the back // the shutter, on both faces, with its window float sh = bx2(q - vec2(0.0, 36.0), vec2(21.0, 10.5), fp); // the two moulded corner holes float holes = bx2(vec2(abs(q.x) - 38.0, q.y + 41.5), vec2(3.4, 3.4), fp); if (n.y > 0.0){ // ---- label side ---- // Nobody ever stuck one on straight, and paper sits proud of the // shell, so it casts a hairline down two of its sides. float la = (hash11(gDye * 3.3 + gLabel * 1.9 + 2.0) - 0.5) * 0.10; vec2 lq = q - vec2(0.0, -12.0); lq = vec2(lq.x * cos(la) + lq.y * sin(la), lq.y * cos(la) - lq.x * sin(la)); float lb = bx2(lq, vec2(32.0, 28.0), fp); float ink; vec3 art = floppyLabel(lq, fp, ink); if (ink >= 0.0){ c = mix(c, c * 0.45, max(bx2(lq + vec2(1.0, 1.0), vec2(32.6, 28.6), fp) - lb, 0.0)); c = mix(c, art, lb); rough = mix(rough, 0.62, lb * (1.0 - max(ink, 0.0) * 0.7)); } // the moulded insertion arrow, up in the corner vec2 aq = q - vec2(-35.0, 27.0); float arw = smoothstep(fp, -fp, max(abs(aq.x) * 1.7 + aq.y - 3.0, -aq.y - 3.0)); c = mix(c, c * 1.35 + 0.012, arw); } else { // ---- hub side ---- float hub = smoothstep(fp, -fp, length(q) - 12.5); c = mix(c, vec3(0.55, 0.56, 0.59) * (0.85 + 0.3 * vnoise(q * 3.0)), hub); c = mix(c, vec3(0.03), bx2(q - vec2(6.5, 0.0), vec2(2.3, 5.2), fp)); rough = mix(rough, 0.22, hub); // the moulded ring the liner is welded round, and the two // stiffening ribs every shell has on the back c = mix(c, c * 0.80, smoothstep(0.55 + fp, 0.12, abs(length(q) - 21.0))); c = mix(c, c * 0.88, smoothstep(0.45 + fp, 0.10, abs(abs(q.x) - 33.0)) * smoothstep(-40.0, -34.0, q.y)); } // the shutter goes over everything, with the folded lip along its // bottom edge and the notch the drive spring sits in vec3 steel = (gDye > 5.5 || gDye < 0.5) ? vec3(0.10, 0.10, 0.11) : vec3(0.60, 0.615, 0.645); c = mix(c, steel * (0.88 + 0.24 * vnoise(vec2(q.x * 6.0, q.y * 0.7))), sh); c = mix(c, steel * 1.45, sh * smoothstep(0.7 + fp, 0.15, abs(q.y - 25.7))); c = mix(c, steel * 0.55, sh * bx2(q - vec2(18.0, 41.0), vec2(2.6, 3.2), fp)); c = mix(c, vec3(0.02), bx2(q - vec2(-6.0, 36.0), vec2(9.5, 7.0), fp) * sh); rough = mix(rough, 0.16, sh); c = mix(c, vec3(0.015), holes); // and one corner hole usually has its slider still in it if (hash11(gDye * 7.1 + gLabel * 2.3 + 13.0) > 0.45) c = mix(c, shell * 1.9 + 0.02, bx2(vec2(q.x + 38.0, q.y + 41.5), vec2(3.0, 3.0), fp)); } else { // ---- the edges: a seam runs round the middle ---- c *= 0.86; c = mix(c, shell * 0.45, smoothstep(0.35 + fp, 0.1, abs(p.y))); } vec3 dl = lp3 - p; float d2 = dot(dl, dl); vec3 L = dl * inversesqrt(d2); float irr = uLampPower * 30000.0 / max(d2, 1.0); float ndl = max(dot(n, L), 0.0); float fres = 0.04 + 0.96 * pow(1.0 - max(dot(n, -rd), 0.0), 5.0); return c * (ndl * irr * 0.34 + uAmbient * 0.75) + vec3(0.96, 0.96, 1.0) * ggx(L, -rd, n, rough) * ndl * irr * 0.085 + studioEnv(gM * reflect(rd, n)) * (0.055 + 0.40 * fres) * mix(0.8, 1.7, 1.0 - rough); } // ---- the 1.2mm edge ---- vec3 shadeRim(vec3 p, vec3 rd, vec3 lp){ vec3 N = normalize(vec3(p.x, 0.0, p.z)); vec3 dl = lp - p; float d2 = dot(dl, dl); vec3 L = dl * inversesqrt(d2); float irr = uLampPower * 30000.0 / max(d2, 1.0); float ndl = max(dot(N, L), 0.0); float fres = 0.05 + 0.95 * pow(1.0 - max(dot(N, -rd), 0.0), 5.0); return studioEnv(gM * reflect(rd, N)) * (0.35 + 1.5 * fres) + vec3(1.0, 0.99, 0.96) * ggx(L, -rd, N, 0.16) * ndl * irr * fres * 1.10 + vec3(0.50, 0.58, 0.72) * ndl * irr * 0.045; } // ---- one object in the rain ---- // What varies between objects is a hash of the index; what varies // with time is a whole number of cycles per minute, so the shower // still closes its loop. It is recomputed per object per pixel, // which is wasteful -- the transforms depend only on the clock -- // but a fragment shader has nowhere to cache them. void discAt(float fi, float t, out vec3 pos, out mat3 M, out float lab, out float dye, out float burn, out float kind){ // Placement is a low-discrepancy sequence, not a hash: multiples // of an irrational, about as evenly spread as a sequence gets and // evenly for every prefix, so turning OBJECTS down thins the // shower rather than tearing a hole in it. The constants also stop // anything INTERSECTING -- everything falls at one speed, so a // close pair stays close for the whole loop, and the obvious // triple resonated at stride 7 and put fifteen pairs through each // other. These leave 182mm between the nearest two. float h0 = fract(fi * 0.4142135624 + 0.13); // column float h1 = fract(fi * 0.6823278038 + 0.41); // depth float h2 = fract(fi * 0.3819660113 + 0.27); // where in its fall float h4 = fract(fi * 0.7320508076 + 0.62); // dye, and where its burn began float h5 = fract(fi * 0.7548776662 + 0.88); // precession count float fall = fract(h2 + t * uFall / 60.0); pos = vec3((h0 - 0.5) * uSpread, mix(uSpread * 1.25, -uSpread * 1.25, fall), (h1 - 0.5) * uSpread * 0.80 - uSpread * 0.12); // Each disc gets its own precession count, so they are never in // step, and each solves its own nutation angle back out of an // integer body count the way the single disc did. float P = max(floor(uPrecess + 0.5) + floor(h5 * 6.0), 1.0); float S = floor(P * cos(radians(uWobble)) * 0.5 + 0.5); float thN = acos(clamp(2.0 * S / P, 0.0, 1.0)); float phi = TAU * (P * t / 60.0 + h0 * 3.13); float psi = -TAU * (S * t / 60.0 + h1 * 4.71); vec3 n = vec3(sin(thN) * cos(phi), cos(thN), sin(thN) * sin(phi)); vec3 nodeL = cross(vec3(0.0, 1.0, 0.0), n); float nl = length(nodeL); nodeL = (nl > 1e-4) ? nodeL / nl : vec3(1.0, 0.0, 0.0); vec3 bx = nodeL * cos(psi) + cross(n, nodeL) * sin(psi); M = mat3(bx, n, cross(n, bx)); lab = mod(fi * 9.0 + 3.0, 22.0); dye = (float(uDye) > 3.5) ? mod(fi * 3.0 + 1.0, 4.0) : float(uDye); burn = fract(uBurn + h4 * 0.83); // A floppy re-uses lab and dye for its own label and shell, which // saves threading two more values through everything. kind = (fract(fi * 0.7182818285 + 0.77) < uFloppy) ? 1.0 : 0.0; if (kind > 0.5){ lab = mod(fi * 4.0 + 1.0, 9.0); dye = mod(fi * 3.0 + 2.0, 7.0); } } vec3 tonemap(vec3 x){ return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); } void main(){ vec2 fragCoord = gl_FragCoord.xy; vec2 R = RENDERSIZE.xy; vec2 uv = (fragCoord - 0.5 * R) / R.y; float t = TIME * uSpeed; vec3 lpw = lampPos(t); // ---- camera, in world ---- float pit = radians(uCamPitch); vec3 ro = vec3(sin(uCamYaw) * cos(pit), sin(pit), cos(uCamYaw) * cos(pit)) * uCamDist; vec3 ww = normalize(vec3(0.0, 0.0, 0.0) - ro); vec3 uu = normalize(cross(ww, vec3(0.0, 1.0, 0.0))); vec3 vv = cross(uu, ww); float f = 0.5 / tan(radians(uFov) * 0.5); // uv is +-0.5, not +-1 vec3 rd = normalize(uv.x * uu + uv.y * vv + f * ww); vec3 rdx = normalize((uv.x + 1.0 / R.y) * uu + uv.y * vv + f * ww); vec3 rdy = normalize(uv.x * uu + (uv.y + 1.0 / R.y) * vv + f * ww); // ---- nearest disc, over three surfaces each ---- float bestT = 1e9, kind = 0.0; mat3 bM = mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0); vec3 bPos = vec3(0.0); float bLab = 0.0, bDye = 0.0, bBurn = 0.0, bKind = 0.0; for (int i = 0; i < NDISC; i++){ float fi = float(i); if (fi > uCount - 0.5) break; vec3 pos; mat3 M; float lab, dye, burn, knd; discAt(fi, t, pos, M, lab, dye, burn, knd); vec3 roD = (ro - pos) * M; // orthonormal, so the transpose vec3 rdD = rd * M; if (knd > 0.5){ // ---- a floppy: a box, clipped by the cut corner ---- vec3 iv = 1.0 / (rdD + vec3(1e-7)); vec3 bh = vec3(F_W, F_T, F_H); vec3 ta = (-bh - roD) * iv, tb = (bh - roD) * iv; vec3 tsm = min(ta, tb), tbg = max(ta, tb); float tin = max(max(tsm.x, tsm.y), tsm.z); float tout = min(min(tbg.x, tbg.y), tbg.z); // the chamfer, as a half-space on the diagonal vec2 nc = vec2(0.7071, 0.7071); float kc = 0.7071 * (F_W + F_H) - 5.0; float dn = dot(rdD.xz, nc), dp = kc - dot(roD.xz, nc); if (abs(dn) > 1e-6){ float tc = dp / dn; if (dn > 0.0) tout = min(tout, tc); else tin = max(tin, tc); } else if (dp < 0.0) { tout = -1.0; } if (tout > max(tin, 0.0) && tin < bestT){ bestT = tin; kind = 4.0; bM = M; bPos = pos; bLab = lab; bDye = dye; bBurn = burn; bKind = 1.0; } } else { if (rdD.y < -1e-5){ // the read face float tt = (TH * 0.5 - roD.y) / rdD.y; float rT = length((roD + rdD * tt).xz); if (tt > 0.0 && tt < bestT && rT > R_HOLE && rT < R_OUT){ bestT = tt; kind = 1.0; bM = M; bPos = pos; bLab = lab; bDye = dye; bBurn = burn; bKind = 0.0; } } if (rdD.y > 1e-5){ // the printed face float tt = (-TH * 0.5 - roD.y) / rdD.y; float rT = length((roD + rdD * tt).xz); if (tt > 0.0 && tt < bestT && rT > R_HOLE && rT < R_OUT){ bestT = tt; kind = 2.0; bM = M; bPos = pos; bLab = lab; bDye = dye; bBurn = burn; bKind = 0.0; } } { // the 1.2mm edge vec2 o2 = roD.xz, d2 = rdD.xz; float qa = dot(d2, d2), qb = dot(o2, d2), qc = dot(o2, o2) - R_OUT * R_OUT; float h = qb * qb - qa * qc; if (h > 0.0){ float tt = (-qb - sqrt(h)) / qa; float y = roD.y + tt * rdD.y; if (tt > 0.0 && tt < bestT && abs(y) < TH * 0.5){ bestT = tt; kind = 3.0; bM = M; bPos = pos; bLab = lab; bDye = dye; bBurn = burn; bKind = 0.0; } } } } } vec3 col, glow = vec3(0.0); if (kind < 0.5){ col = vec3(0.0); } else { gM = bM; gLabel = bLab; gDye = bDye; gBurn = bBurn; gKind = bKind; vec3 roD = (ro - bPos) * bM; vec3 rdD = rd * bM; vec3 rdxD = rdx * bM; vec3 rdyD = rdy * bM; vec3 lpD = lpw * bM; // rigged per disc, not to the room if (bKind > 0.5){ // recover which face of the box was hit from the slab that won vec3 pb = roD + rdD * bestT; vec3 an = abs(pb) / vec3(F_W, F_T, F_H); vec3 nb = -sign(rdD) * step(an.yzx, an.xyz) * step(an.zxy, an.xyz); if (dot(pb.xz, vec2(0.7071)) > 0.7071 * (F_W + F_H) - 5.2) nb = vec3(0.7071, 0.0, 0.7071); col = shadeFloppy(pb, normalize(nb), rdD, lpD, length((roD + rdxD * bestT).xz - pb.xz)); } else if (kind < 1.5){ col = shadeDisc(roD, rdD, rdxD, rdyD, lpD, t, glow); } else if (kind < 2.5){ vec3 pb = roD + rdD * bestT; vec3 pbx = roD + rdxD * ((-TH * 0.5 - roD.y) / rdxD.y); col = shadeLabel(pb, rdD, lpD, length(pbx.xz - pb.xz)); } else { col = shadeRim(roD + rdD * bestT, rdD, lpD); } } col += glow * uBloom; col *= uExposure; col = tonemap(col); col = pow(max(col, 0.0), vec3(0.4545)); col *= 1.0 - uVignette * 0.65 * dot(uv, uv); col += (hash12(fragCoord + fract(t) * 71.0) - 0.5) * 0.05; // fixed film grain gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0); }
forking #oyj85kdj — lineage will be recorded.
renders in YOUR browser. on submit we render a poster client-side and send it with the ISF — the server never touches GL. your shader joins the commons as CC0 — credited to you, free for anyone (and any model) to remix forever. that's the whole point. it's permanent: once it's public, CC0 has no take-backs. by submitting you attest you have the right to release it as CC0.