/*{ "DESCRIPTION": "THE CATHEDRAL POOLROOMS. An endless flooded cathedral in ankle-deep water, generated rather than modelled: no level file and no mesh, every pier and every arcade opening decided by hashing an integer cell index, so the building is unbounded and identical for everyone who runs it. The plan is still the backrooms trick - a 2D box SDF over the 3x3 cells around the sample, extruded from the floor to the impost line - but the lid is gone. THE CEILING IS THE WORK. A drop ceiling is a plane and costs nothing; a vault is a shape, and the only vault that can tile an infinite building is one written as a single folded cell, so p.xz is folded into one bay and the vault is drawn ONCE, everywhere. Two things fall straight out of that: no arch can ever collide with a neighbour, because there is only ever one arch; and the cost of an infinite cathedral is the cost of one bay. The cell is a groin vault, which is the UNION of two barrel vaults and not their intersection - the intersection is a cloister vault, it closes to a point at the crown, and that is a dome and not a cathedral. Each barrel is a POINTED arch, built as a circle whose centre is pushed outward past the springing by uPoint, so the two arcs meet at an angle instead of a tangent; the crown then rises as sqrt(1 + 2*uPoint) times the half-bay, which is the entire reason the pointed arch was invented and the reason the ceiling here stands three times the height of the walls carrying it. It is not a height field. The obvious way to arch a ceiling is to make its height a function of x and z and subtract that, and such a profile is VERTICAL at the springing, so its gradient is unbounded exactly where the geometry is most delicate and a sphere tracer walks straight through the haunch. A barrel written as a radius about an axis has gradient one everywhere. Folding has one failure and it is instructive: below the springing line the barrel field degenerates to a slab, and the max of two slabs is a square shaft that would wall off every bay at floor level. So the void is unioned with the whole half-space beneath the springing - which is exact rather than a fudge, because every scrap of vault masonry lives above that plane - and the union leaves precisely one zero, at the corner of each bay at impost height. That zero is not an artefact to be smoothed away, it is where the vault lands: put a corbel there and the field becomes honest. The ribs are subtracted tubes whose axes are curves the surfaces already share - the groin where the two barrels cross, the transverse arch in the plane of the bay edge, the ridge along each crown, a boss at their crossing, and a pair of intermediate liernes that turn a quadripartite vault into a net vault as uRibs is raised. The webbing between them is coursed ashlar laid in the vault's OWN coordinates, arc length up the curve against distance along the axis, so the courses run with the shape instead of across it. The lights are floating orbs and they are deliberately NOT in the distance field. An emissive sphere in map() is found by every shadow ray and every ambient-occlusion probe, and a light that occludes itself blacks out the room it is lighting; so each orb is an analytic splat along the ray - one dot product for depth, one for the perpendicular miss - and a point in the light gather, and nothing else. Their colour is a blackbody curve in kelvin, normalised so that changing the temperature changes the colour and not the exposure, from 1700 K candle to 7500 K overcast. Around each orb is a flock of moths. Each moth is on a tilted, precessing orbit whose radius breathes, so the flock spirals toward the light and falls away from it rather than sitting on a ring; they are projected onto the ray and accumulated as ONE order-independent premultiplied layer, which is what lets a moth be a bright mote against the dark nave and a dark silhouette across the orb without ever being sorted. The water is unchanged and still the subject, and still not marched: a pool ripple is 8 mm, under a pixel past arm's length, so the surface is an analytic plane hit plus one Newton step and everything anyone sees is in the NORMAL. Four rotated wave trains, phase-warped by value noise, each moving at one over the square root of its wavenumber - gravity-wave dispersion - so the long trains outrun the short ones and the field never reads as a scrolling texture. Caustics are the focusing term of the refracted map, one over one plus depth times the Laplacian of the ripple, and the same field attenuated by height is what puts moving light forty feet up on the vault webbing. The picture is graded warm and graded in LINEAR, which is the only place a grade is a multiply rather than a wish: the gain is warmed and the lift is cooled BEFORE the tone curve, and the curve itself is a fit to the ACES output transform rather than the Reinhard divide it replaces. Reinhard has neither a toe nor a shoulder, and the shoulder is the whole reason a light reads as a light instead of a white disc with a hard rim - which matters more here than anywhere, because the subject of the room is forty floating lights. Warm orbs over cold water is also the oldest trick in colour film, and it is free: the orbs are tungsten by default and the water keeps the blue-green it gets from Beer-Lambert, so the two ends of the frame separate without a single hand-placed colour. Every tile in the building is a HEXAGON: a two-sublattice fold, a hexagonal norm for the joint distance, and the same box filter as before, decaying to the mosaic's duty cycle rather than to white, because a periodic grid seen edge-on averages to grey. Forty-seven controls: walk, lens, yaw and pitch, wander and turn, sway, cell size, wall density, closed chapels, arcade frequency and width, impost height, vault bay, arch point, rib density, ledge, water depth, ripple, flow, caustics, clarity, orb spacing, working orbs, orb height, orb size, colour temperature, brightness, dark zones, moths, swarm radius, haze, fog, four palettes, filmic curve, print warmth, mosaic scale, accent course, grime, damp, ambient occlusion, shadow, gloss, and four camcorder controls.", "CREDIT": "CC0. A fork of THE POOLROOMS. Plan generation, footprint filtering and the light gather descend from the CC0 BACKROOMS shader; the vault, the orbs and the moths are original; the water is informed by but not derived from Alexander Alekseev's Seascape.", "CATEGORIES": ["generator", "raymarch", "liminal", "cathedral", "vault", "gothic", "water", "caustics", "moths", "procedural", "3d"], "INPUTS": [ { "NAME": "uSpeed", "TYPE": "float", "DEFAULT": 2.0, "MIN": 0.0, "MAX": 5.0 }, { "NAME": "uLens", "TYPE": "float", "DEFAULT": 1.40, "MIN": 0.7, "MAX": 3.0 }, { "NAME": "uYaw", "TYPE": "float", "DEFAULT": 0.0, "MIN": -1.57, "MAX": 1.57 }, { "NAME": "uPitch", "TYPE": "float", "DEFAULT": 0.08, "MIN": -0.6, "MAX": 0.6 }, { "NAME": "uWander", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 1.6 }, { "NAME": "uTurn", "TYPE": "float", "DEFAULT": 0.45, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uBob", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uCell", "TYPE": "float", "DEFAULT": 5.5, "MIN": 3.5, "MAX": 9.0 }, { "NAME": "uDensity", "TYPE": "float", "DEFAULT": 0.58, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uRooms", "TYPE": "float", "DEFAULT": 0.22, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uArch", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uArchW", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.40, "MAX": 1.10 }, { "NAME": "uHeight", "TYPE": "float", "DEFAULT": 4.20, "MIN": 2.6, "MAX": 8.0 }, { "NAME": "uBay", "TYPE": "float", "DEFAULT": 2.0, "MIN": 1.0, "MAX": 3.0 }, { "NAME": "uPoint", "TYPE": "float", "DEFAULT": 0.55, "MIN": 0.0, "MAX": 1.6 }, { "NAME": "uRibs", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uLedge", "TYPE": "float", "DEFAULT": 0.26, "MIN": 0.0, "MAX": 0.60 }, { "NAME": "uWater", "TYPE": "float", "DEFAULT": 0.42, "MIN": 0.0, "MAX": 1.10 }, { "NAME": "uRipple", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 2.5 }, { "NAME": "uFlow", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uCaustic", "TYPE": "float", "DEFAULT": 0.85, "MIN": 0.0, "MAX": 2.0 }, { "NAME": "uClarity", "TYPE": "float", "DEFAULT": 0.55, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uLightSpace", "TYPE": "float", "DEFAULT": 5.50, "MIN": 2.0, "MAX": 14.0 }, { "NAME": "uLightOn", "TYPE": "float", "DEFAULT": 0.85, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uOrbY", "TYPE": "float", "DEFAULT": 2.40, "MIN": 1.0, "MAX": 6.0 }, { "NAME": "uOrbSize", "TYPE": "float", "DEFAULT": 0.11, "MIN": 0.03, "MAX": 0.35 }, { "NAME": "uTemp", "TYPE": "float", "DEFAULT": 4000.0,"MIN": 1700.0,"MAX": 7500.0 }, { "NAME": "uBright", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uZones", "TYPE": "float", "DEFAULT": 0.35, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uMoths", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uMothR", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.15, "MAX": 2.0 }, { "NAME": "uHaze", "TYPE": "float", "DEFAULT": 0.75, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uFog", "TYPE": "float", "DEFAULT": 0.035, "MIN": 0.0, "MAX": 0.2 }, { "NAME": "uPalette", "TYPE": "float", "DEFAULT": 0.0, "MIN": 0.0, "MAX": 3.0 }, { "NAME": "uTileScale", "TYPE": "float", "DEFAULT": 1.0, "MIN": 0.5, "MAX": 2.5 }, { "NAME": "uAccent", "TYPE": "float", "DEFAULT": 0.70, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGrime", "TYPE": "float", "DEFAULT": 0.35, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uDamp", "TYPE": "float", "DEFAULT": 0.40, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uAO", "TYPE": "float", "DEFAULT": 0.75, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uShadow", "TYPE": "float", "DEFAULT": 0.80, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGloss", "TYPE": "float", "DEFAULT": 0.80, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uFilm", "TYPE": "float", "DEFAULT": 0.90, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uWarm", "TYPE": "float", "DEFAULT": 0.85, "MIN": 0.0, "MAX": 1.5 }, { "NAME": "uVHS", "TYPE": "float", "DEFAULT": 0.30, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uScan", "TYPE": "float", "DEFAULT": 0.22, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uGrain", "TYPE": "float", "DEFAULT": 0.20, "MIN": 0.0, "MAX": 1.0 }, { "NAME": "uVignette", "TYPE": "float", "DEFAULT": 0.45, "MIN": 0.0, "MAX": 1.0 } ] }*/ // THE CATHEDRAL POOLROOMS - ISF / GLSL ES 1.00 (gl_FragColor, TIME, RENDERSIZE). // // An infinite flooded cathedral in ankle-to-knee-deep water. The plan and the // footprint filtering descend from the BACKROOMS shader; the vault, the orbs // and their moths are new, and the water is an analytic surface whose entire // appearance lives in its normal. #define PI 3.14159265359 #define FAR 48.0 // Mosaic modules, before uTileScale. These are ACROSS-FLATS distances: the // hexagonal norm below is built with an inradius of 0.5, so dividing world // metres by T makes T the width of one hexagon measured flat to flat. #define HWALL 0.052 #define HFLOR 0.078 // coursed ashlar for the vault webbing: course height and stone length #define ASH_H 0.34 #define ASH_L 0.78 float gLG; // orb lattice spacing (metres) float gOrbR; // orb radius float gBay; // vault bay, a whole number of plan cells float gRb; // half the bay - the springing half-span float gOff; // pointed-arch centre offset float gYs; // springing / impost height float gTc; // crown height above the springing float gRibR; // primary rib radius float gWL; // still-water level float gWT; // water clock vec2 gCam; // camera plan position, for the wake float gPX; // pixel solid angle scale, for the footprint // palette, filled once in main vec3 gTile, gGrout, gAcc, gStone, gLite, gAmb, gShal, gDeep; // --------------------------------------------------------------- // hashes and value noise (Dave Hoskins' constants) // --------------------------------------------------------------- float hash11(float p){ p = fract(p*0.1031); p *= p + 33.33; p *= p + p; return fract(p); } float hash12(vec2 p){ vec3 q = fract(vec3(p.xyx)*0.1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y)*q.z); } vec4 hash42(vec2 p){ vec4 q = fract(vec4(p.xyxy)*vec4(0.1031, 0.1030, 0.0973, 0.1099)); q += dot(q, q.wzxy + 33.33); return fract((q.xxyz + q.yzzw)*q.zywx); } 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); } // Every octave rotated as well as scaled: value noise has axis-aligned // iso-contours, and four unrotated octaves stack their cell edges into a // visible quilt on any large flat surface. float fbm(vec2 p){ mat2 R = mat2(0.8040, 0.5946, -0.5946, 0.8040); float s = 0.0, a = 0.5; for (int i = 0; i < 4; i++){ s += a*vnoise(p); p = R*p*2.07 + 19.3; a *= 0.5; } return s; } // --------------------------------------------------------------- // blackbody // --------------------------------------------------------------- // A light's TEMPERATURE should change its colour and not its exposure, so the // curve is normalised by its own maximum channel and then pulled back to unit // luminance. Without that second step the warm end of the slider is also the // dim end, and every other control in the shader appears to drift with it. vec3 kelvin(float k){ k = clamp(k, 1000.0, 15000.0)*0.01; float r, g, b; if (k <= 66.0){ r = 1.0; g = clamp(0.39008158*log(max(k, 1.0)) - 0.63184144, 0.0, 1.0); } else { r = clamp(1.29293619*pow(max(k - 60.0, 0.001), -0.1332047), 0.0, 1.0); g = clamp(1.12989086*pow(max(k - 60.0, 0.001), -0.0755148), 0.0, 1.0); } if (k >= 66.0) b = 1.0; else if (k <= 19.0) b = 0.0; else b = clamp(0.54320679*log(max(k - 10.0, 1.0)) - 1.19625408, 0.0, 1.0); vec3 c = vec3(r, g, b)/max(max(r, g), max(b, 1e-3)); float l = dot(c, vec3(0.299, 0.587, 0.114)); return c*(0.92/max(l, 1e-3)); } // --------------------------------------------------------------- // primitives and the footprint filter // --------------------------------------------------------------- float sdBox2(vec2 p, vec2 b){ vec2 q = abs(p) - b; return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0); } // Box-filter coverage of the band |x| < r by a pixel w wide. This is the part // a fixed smoothstep cannot do: a mosaic joint is 2 mm across, so past a // couple of metres - and immediately on any wall seen obliquely - it is // thinner than a pixel, and a hard-edged test keeps drawing it at full // strength wherever it lands, which beats against the pixel grid as moire. float band(float x, float r, float w){ float h = 0.5*max(w, 1e-5); return clamp((min(x + h, r) - max(x - h, -r))/(2.0*h), 0.0, 1.0); } // How much of a texture is safe to draw. The transition has to be WIDE: // everything keys its strength off the footprint, and since the footprint // depends on dot(n, rd), a narrow fade paints conics about the normal - which // is to say concentric rings across every oblique wall. float mip(float feature, float foot){ return smoothstep(1.10, 0.10, foot/feature); } vec3 pick4(vec3 a, vec3 b, vec3 c, vec3 d, float f){ float i = clamp(f, 0.0, 3.0); float n = floor(i), k = i - n; vec3 lo = a; lo = mix(lo, b, step(0.5, n)); lo = mix(lo, c, step(1.5, n)); lo = mix(lo, d, step(2.5, n)); vec3 hi = b; hi = mix(hi, c, step(0.5, n)); hi = mix(hi, d, step(1.5, n)); return mix(lo, hi, k); } // --------------------------------------------------------------- // the grade // --------------------------------------------------------------- // Reinhard is a curve that never clips and never contrasts. Film has a TOE and // a SHOULDER, and the shoulder is the reason a light source reads as a light // source instead of a white disc with a hard rim - which matters more in this // room than in most, because the subject of it is forty floating lights. This // is Krzysztof Narkowicz's fit to the ACES output transform: three // multiply-adds and a divide, and it does more for the picture than any amount // of extra marching would. vec3 filmic(vec3 x){ x *= 0.62; return clamp((x*(2.51*x + 0.03))/(x*(2.43*x + 0.59) + 0.14), 0.0, 1.0); } // Warm the gain, cool the lift, and do it in LINEAR - before the curve, where // multiplying the highlights by an amber and leaving a cold trace in the black // is what a print actually does. The same two numbers applied after the tone // map are a tint smeared over data that has already been crushed. vec3 warmGrade(vec3 c, float w){ float l = dot(c, vec3(0.2126, 0.7152, 0.0722)); vec3 gain = mix(vec3(1.0), vec3(1.10, 1.00, 0.87), w); vec3 lift = vec3(0.006, 0.013, 0.030)*w; return c*gain + lift*(1.0 - smoothstep(0.0, 0.45, l)); } // --------------------------------------------------------------- // the hexagonal mosaic // --------------------------------------------------------------- // A hex lattice is two interleaved rectangular lattices, so the fold is two // floors and one comparison: whichever candidate centre is nearer owns the // point. Cheaper and far more robust than any of the axial-coordinate // rounding schemes, and it hands back an integer-ish id to hash. // // .xy = position relative to the hexagon's centre, .zw = the hexagon's id vec4 hexCell(vec2 p){ vec2 s = vec2(1.0, 1.7320508); vec2 o = vec2(0.5, 0.8660254); // the offset between the two lattices vec2 iA = floor(p/s) + 0.5; vec2 iB = floor((p - o)/s) + 0.5; vec2 hA = p - iA*s; vec2 hB = p - (iB*s + o); return dot(hA, hA) < dot(hB, hB) ? vec4(hA, iA) : vec4(hB, iB + 0.5); } // The hexagonal norm: exactly 0.5 on all six edges, so 0.5 minus it is the // true perpendicular distance to the nearest joint. Pointy-top, one unit // across the flats, which is what makes T in hexJoint a real width in metres. float hexNorm(vec2 p){ p = abs(p); return max(p.x, dot(p, vec2(0.5, 0.8660254))); } // A periodic grid seen edge-on averages to its DUTY CYCLE, not to nothing. // Letting band() run to zero on its own would bleach every distant wall to // plain white; blending toward the average joint coverage as the footprint // outgrows the tile is what makes a far wall read as mosaic you cannot // resolve. For a hexagon of inradius 0.5 that coverage is perimeter*gr/area, // which works out to 4*gr - the same number as the square grid it replaces. float hexJoint(vec2 uv, float T, float foot, out vec2 tid, out vec2 tf){ vec4 hc = hexCell(uv/T); tid = hc.zw; tf = hc.xy; float e = 0.5 - hexNorm(hc.xy); float w = foot/T; float gr = 0.048; return mix(band(e, gr, w), 4.0*gr, smoothstep(0.06, 0.40, w)); } // --------------------------------------------------------------- // coursed ashlar, for the vault webbing // --------------------------------------------------------------- // Stone is not tile: the courses are wide, the perpends are staggered, and the // joint is a recessed line rather than a grid. Same box filter, same decay to // the duty cycle. float ashlar(vec2 uv, float ch, float cw, float foot, out vec2 tid){ float row = floor(uv.y/ch); float off = 0.5*cw*mod(row, 2.0) + 0.31*cw*hash11(row*1.37); float col = floor((uv.x + off)/cw); tid = vec2(col, row); float ey = (0.5 - abs(fract(uv.y/ch) - 0.5))*ch; float ex = (0.5 - abs(fract((uv.x + off)/cw) - 0.5))*cw; float e = min(ey, ex); float gr = 0.011; float duty = clamp(gr*(2.0/ch + 2.0/cw), 0.0, 1.0); return mix(band(e, gr, foot), duty, smoothstep(0.35*ch, 1.6*ch, foot)); } // --------------------------------------------------------------- // the plan // --------------------------------------------------------------- // The walk and the open lane are the same curve, x = pathX(z), so the nave is // carved by the very function that drives the camera and can never be walled // off. float pathX(float z){ return uWander*(2.6*sin(z*0.0290) + 1.35*sin(z*0.0161 + 1.7)) + uTurn*(11.0*sin(z*0.0473 + 2.1) + 7.0*sin(z*0.0281 + 5.3) + 4.5*sin(z*0.0917 + 0.7)); } float pathSlope(float z){ return uWander*(0.0754*cos(z*0.0290) + 0.0217*cos(z*0.0161 + 1.7)) + uTurn*(0.5203*cos(z*0.0473 + 2.1) + 0.1967*cos(z*0.0281 + 5.3) + 0.4127*cos(z*0.0917 + 0.7)); } // THE ARCADE OPENING, as a cross-section rather than as a width. // // The obvious way to arch an opening is to make its half-width a function of // height - W*sqrt(1 - u*u) above the springing - and subtract that box. It // looks right and it destroys the march: that profile has infinite slope at // the crown, so the field's Lipschitz constant blows up exactly there and the // sphere tracer steps straight through the arch head. // // So the section is built properly, in the plane of the wall: a jamb box // unioned with the two arcs of a pointed arch, which is a valid SDF // everywhere, then extruded through the wall by a max against the // through-thickness. Same silhouette, bounded gradient, no artefact at the // crown - and the point at the head now matches the vault overhead, which a // semicircle did not. // // u = along the wall, v = through it, y = height float archSection(float u, float v, float y, float thick){ float W = uArchW; float S = 1.02 + 0.25*W; // springing line float o = W*uPoint*0.75; // the same trick as the vault float jamb = sdBox2(vec2(u, y - 0.5*S), vec2(W, 0.5*S)); float head = length(vec2(abs(u) + o, max(y - S, 0.0))) - (W + o); return max(min(jamb, head), abs(v) - thick); } // .x = pier and screen-wall footprint (2D), .y = the arched opening (3D) vec2 maze2(vec2 q, float y){ float C = uCell; vec2 base = floor(q/C); float dW = C, dD = C; for (int j = 0; j < 3; j++){ // the lane depends only on the row, so the path is evaluated once here float rz = (base.y + float(j) - 0.5)*C; float px = pathX(rz); float sl = pathSlope(rz); // A band of fixed horizontal width is a nave that narrows as the path // steepens; widening it by sqrt(1 + slope^2) keeps the lane a constant // width measured ACROSS itself. float pw = 0.80*C*sqrt(1.0 + sl*sl); for (int i = 0; i < 3; i++){ vec2 id = base + vec2(float(i) - 1.0, float(j) - 1.0); vec2 c = (id + 0.5)*C; vec4 h = hash42(id); vec4 g = hash42(id + 57.31); float lane = step(pw, abs(c.x - px)); float on = step(h.x, uDensity)*lane; float horiz = step(0.5, h.y); vec2 dirL = mix(vec2(0.0, 1.0), vec2(1.0, 0.0), horiz); vec2 dirT = mix(vec2(1.0, 0.0), vec2(0.0, 1.0), horiz); float pil = step(0.90, g.x); // a square pier float len = mix((0.66 + 0.34*h.z)*0.5*C, 0.32, pil); float th = mix(0.115, 0.36, pil); vec2 ctr = c + dirL*((h.w - 0.5)*(C - 2.0*len)) + dirT*((g.y - 0.5)*0.70*(C - 2.0*th)); // Some cells are a closed chapel rather than a free-standing // screen: a rectangular ring, the difference of two boxes. // Without it the plan is only ever scattered slabs and you never // end up anywhere with four walls around you. float isRoom = step(fract(h.y*31.7), uRooms); float rr = 0.5*C - 0.22; float ring = max(sdBox2(q - c, vec2(rr)), -sdBox2(q - c, vec2(rr - 0.11))); float slab = sdBox2(q - ctr, dirL*len + dirT*th); dW = min(dW, mix(slab, ring, isRoom) + (1.0 - on)*90.0); // A chapel ALWAYS gets its opening, or it is a sealed box nobody // can ever be inside; a screen wall gets one with probability // uArch. float dr = on*mix(step(g.z, uArch)*(1.0 - pil), 1.0, isRoom); vec2 dc = mix(ctr + dirL*((g.w - 0.5)*2.0*max(len - 1.15, 0.0)), c + dirT*rr, isRoom); vec2 rel = q - dc; float u = dot(rel, dirL), v = dot(rel, dirT); float thick = mix(th, 0.11, isRoom) + 0.55; // cut clean through dD = min(dD, archSection(u, v, y, thick) + (1.0 - dr)*90.0); if (dW < -0.5*C) break; } if (dW < -0.5*C) break; } return vec2(min(dW, C), min(dD, C)); } // --------------------------------------------------------------- // THE VAULT // --------------------------------------------------------------- // One pointed barrel: the free-space distance inside a gothic arch of span // 2*gRb springing at t = 0. The circle's centre is pushed outward to -gOff, so // the two arcs cross at an ANGLE over the axis instead of meeting tangentially // - which is what makes the arch point, and what lifts the crown from gRb to // sqrt(gRb*gRb + 2*gRb*gOff). Gradient one everywhere, which a height field // written as a width-versus-height cannot manage anywhere near the springing. float barrel(float u, float t){ return (gRb + gOff) - length(vec2(abs(u) + gOff, t)); } // The entire ceiling, drawn once, in ONE folded bay. Because it is folded, no // arch can collide with a neighbour: there is only ever one arch. `rib` comes // back as the rib solid's own field so the shader can tell dressed stone from // webbing without recomputing any of it. float vaultField(vec3 p, out float rib, out float boss){ vec2 q = mod(p.xz, gBay) - 0.5*gBay; float u = q.x, v = q.y; float tw = p.y - gYs; float t = max(tw, 0.0); float dn = -tw; // depth below the springing line float fa = barrel(u, t), fb = barrel(v, t); // A groin vault is the UNION of two barrels, not their intersection - the // intersection is a cloister vault and it closes to a point at the crown, // which is a dome. Below the springing the folded barrels degenerate to // slabs at |u| = gRb and |v| = gRb, and the max of two slabs is a square // shaft that would wall off every bay at floor level; so the void is // unioned with the whole half-space beneath the springing. That is exact // rather than a fudge, because every scrap of vault masonry lives above // gYs, so a point dn below it is at least dn from all of it. float d = max(max(fa, fb), dn); float Rr = gRb + gOff; // distance to each barrel's own surface, for the ribs that ride on them float pa = abs(length(vec2(abs(u) + gOff, t)) - Rr); float pb = abs(length(vec2(abs(v) + gOff, t)) - Rr); // THE GROIN: the curve the two barrels share, |u| = |v|. Written in the // half-sum and half-difference, which costs a factor of root two in the // gradient - and a field that UNDER-states its distance is exactly the // kind that is safe to march. float a = 0.5*(abs(u) + abs(v)); float b = 0.5*(abs(u) - abs(v)); float rg = length(vec2(length(vec2(a + gOff, t)) - Rr, b)); // the transverse arches, standing in the planes of the bay edges float r3 = length(vec2(pb, gRb - abs(u))); float r4 = length(vec2(pa, gRb - abs(v))); float primary = min(rg, min(r3, r4)) - gRibR; // Ridge ribs along the two crowns, and a pair of liernes crossing the web // between groin and transverse arch. These are what turn a plain // quadripartite vault into a net vault as uRibs comes up, and they are the // whole difference between a ceiling and a cathedral ceiling. float r1 = length(vec2(u, tw - gTc)); float r2 = length(vec2(v, tw - gTc)); float r5 = length(vec2(pb, abs(abs(u) - 0.52*gRb))); float r6 = length(vec2(pa, abs(abs(v) - 0.52*gRb))); float rr2 = gRibR*(0.18 + 0.55*uRibs); float second = min(min(r1, r2), min(r5, r6)) - rr2; rib = min(primary, second); // ribs are solid, and solid only where there is vault for them to hang from float ribD = max(rib, dn); // the boss, at the crossing of the two ridges boss = length(vec3(u, tw - gTc, v)) - gRibR*2.40; d = min(min(d, ribD), boss); // THE CORBEL. Unioning the void with the half-space below the springing // leaves exactly one zero, at the bay corner at t = 0. That zero is not an // artefact to be smoothed away - it is the point the whole vault stands // on. Give it the stone it is asking for and the field stops lying. float dc = length(vec2(gRb - abs(u), gRb - abs(v))); // The corbel is not allowed to hang into head height at a low impost, and // its taper is tied to its length rather than to the rib: shortening the // cone without narrowing it steepens the field past what the march step // can absorb, which is a hole in the stone rather than a shorter bracket. float ch = min(3.2*gRibR, max(gYs - 1.95, 0.25)); float cr0 = min(1.7*gRibR, 0.55*ch); float k = clamp(dn/max(ch, 1e-4), 0.0, 1.0); // A LINEAR taper, to a point. The concave profile of a real corbel is the // prettier stone and it doubles the field's slope at the tip, which the // march step cannot pay for; a straight cone lands on the same silhouette // for a tenth of the trouble. float corb = max(max(dc - cr0*(1.0 - k), -dn), dn - ch); // The liernes come down to the impost too, at their own two stations along // each bay edge, so they get their own smaller springers. A rib that dies // in mid-air at the springing line is the one thing that reads instantly // as generated rather than built. float dl3 = min(length(vec2(gRb - abs(u), abs(abs(v) - 0.52*gRb))), length(vec2(gRb - abs(v), abs(abs(u) - 0.52*gRb)))); float ch2 = ch *0.50*(0.30 + 0.70*uRibs); float cr2 = cr0*0.50*(0.30 + 0.70*uRibs); float k2 = clamp(dn/max(ch2, 1e-4), 0.0, 1.0); float corb2 = max(max(dl3 - cr2*(1.0 - k2), -dn), dn - ch2); d = min(d, min(corb, corb2)*0.80); return d*0.86; // the fold and the corbel each cost a little Lipschitz } // --------------------------------------------------------------- // .x = distance, .y = material (1 floor, 2 vault, 3 wall, 4 ledge) // --------------------------------------------------------------- vec2 map(vec3 p){ vec2 r = vec2(p.y, 1.0); // the flooded floor float rib, boss; float dV = vaultField(p, rib, boss); if (dV < r.x) r = vec2(dV, 2.0); vec2 mz = maze2(p.xz, p.y); // The arcade STOPS at the impost. Above it the whole building is one // volume, which is the entire difference between a corridor with a lid and // a cathedral: you can see the vault run away over the tops of the walls. float dW = max(max(mz.x, -mz.y), p.y - gYs); if (dW < r.x) r = vec2(dW, 3.0); // The ledge is defined relative to the WATER, not to the floor, because // its whole job is to be half in it: a step you could sit on, that gives // the waterline something to cut across other than a flat wall. if (uLedge > 0.02){ float lh = gWL + 0.20; float dl = max(max(mz.x - uLedge, -mz.y), p.y - lh); if (dl < r.x) r = vec2(dl, 4.0); } return r; } vec3 calcNormal(vec3 p){ vec2 e = vec2(1.0, -1.0)*0.0018; return normalize(e.xyy*map(p + e.xyy).x + e.yyx*map(p + e.yyx).x + e.yxy*map(p + e.yxy).x + e.xxx*map(p + e.xxx).x); } float calcAO(vec3 p, vec3 n){ float o = 0.0, s = 1.0; for (int i = 0; i < 5; i++){ float h = 0.02 + 0.16*float(i); o += (h - map(p + n*h).x)*s; s *= 0.72; } return clamp(1.0 - 1.05*o, 0.0, 1.0); } float shadowRay(vec3 ro, vec3 rd, float maxt){ float res = 1.0, t = 0.16, prev = 1e12; for (int i = 0; i < 24; i++){ float h = map(ro + rd*t).x; float y = h*h/(2.0*prev); float d = sqrt(max(h*h - y*y, 0.0)); res = min(res, d/max(0.0001, 7.0*(t - y))); prev = h; t += clamp(h, 0.05, 0.55); if (res < 0.02 || t > maxt) break; } return clamp(res, 0.0, 1.0); } // --------------------------------------------------------------- // the orbs // --------------------------------------------------------------- // A slowly varying field that fails the lights in BLOCKS rather than one at a // time. A flat per-orb probability scatters the dead ones evenly and every // distance comes out equally lit; clumping is what puts a genuinely dark bay // at the end of a nave. The same field dims the ambient and the haze, so a // dark zone is dark the whole way through. float lightZone(vec2 xz){ return smoothstep(0.20, 0.80, vnoise(xz*0.026)); } vec3 orbP(vec2 lid){ vec4 h = hash42(lid + 11.7); return vec3((lid.x + 0.5)*gLG, uOrbY + (h.y - 0.5)*0.85 + 0.13*sin(TIME*(0.45 + 0.35*h.z) + h.w*6.2831), (lid.y + 0.5)*gLG); } float orbOn(vec2 lid){ vec4 h = hash42(lid + 11.7); float zn = lightZone((lid + 0.5)*gLG); float on = step(h.x, uLightOn*mix(1.0, 0.05 + 1.90*zn, uZones)); // not a mains hum any more: an orb breathes return on*(0.86 + 0.14*sin(TIME*(0.9 + 1.4*h.w) + h.z*31.0)); } // Nine nearest orbs gathered, plus ONE direction to fire a single shadow ray // along. That direction is the WEIGHTED MEAN of the nine and not the strongest // of them: the strongest changes as you slide along a wall, the loci where two // tie are smooth curves, and the shadow term steps across them - which paints // the wall in concentric arcs. A mean has no seams. float lighting(vec3 p, vec3 n, out vec3 keyDir, out float keyDist){ vec2 lb = floor(p.xz/gLG); float sum = 0.0, accD = 0.0; vec3 acc = vec3(0.0); for (int j = 0; j < 3; j++){ for (int i = 0; i < 3; i++){ vec2 lid = lb + vec2(float(i) - 1.0, float(j) - 1.0); vec3 pl = orbP(lid); vec3 L = pl - p; float dl = max(length(L), 0.12); L /= dl; // An orb must be worth NOTHING by the time the 3x3 window drops // it, or the sum steps at every lattice edge. float w = orbOn(lid)*max(dot(n, L), 0.0) * smoothstep(1.48*gLG, 1.02*gLG, length(p.xz - pl.xz)) / (1.0 + 0.26*dl*dl); sum += w; acc += L*w; accD += dl*w; } } keyDir = normalize(acc + n*0.001 + vec3(0.0, 0.0001, 0.0)); keyDist = accD/max(sum, 0.0001); return sum; } // --------------------------------------------------------------- // the orbs on the ray, and their moths // --------------------------------------------------------------- // The orbs are NOT in map(). An emissive sphere in the distance field is found // by every shadow ray and every occlusion probe, and a light that occludes // itself blacks out the room it is lighting. So they are splatted along the // ray instead: one dot product for depth, one for the perpendicular miss. // Everything an orb does to the geometry it does through lighting() and // through the in-scattering march, neither of which needs it to be solid. // // The window is twenty-five lattice cells centred on a point DOWN THE RAY // rather than on the camera, so the same twenty-five cover a long nave and a // small chapel; the geometric reject runs before orbOn(), which is the only // part that costs a noise lookup. vec3 orbRay(vec3 ro, vec3 rd, float tmax, float doMoths, out vec3 mCol, out float mA){ mCol = vec3(0.0); mA = 0.0; vec3 add = vec3(0.0); vec2 fw = rd.xz; float fl = length(fw); fw = fl > 1e-4 ? fw/fl : vec2(0.0, 1.0); vec2 piv = ro.xz + fw*clamp(tmax*0.45, 0.9*gLG, 2.4*gLG); vec2 lb = floor(piv/gLG) - 2.0; // A basis across the ray, level with the world, so a moth can be splatted // as a wing-wide ellipse rather than a ball. Hoisted: it depends only on // the ray, and the inner loop runs up to a couple of hundred times. vec3 ex = cross(rd, vec3(0.0, 1.0, 0.0)); ex = normalize(ex + vec3(1e-4, 0.0, 1e-4)); vec3 ey = cross(ex, rd); float reach = max(uMothR*1.55, 9.0*gOrbR) + 0.25; float mothN = floor(uMoths*14.0 + 0.5); for (int j = 0; j < 5; j++){ for (int i = 0; i < 5; i++){ vec2 lid = lb + vec2(float(i), float(j)); vec3 O = orbP(lid); vec3 w = O - ro; float sd = dot(w, rd); float perp = length(w - rd*clamp(sd, 0.0, tmax)); if (perp < reach && sd < tmax + reach && sd > -reach){ float on = orbOn(lid); if (on > 0.01){ // ---- the orb itself ---- if (sd > 0.03 && sd < tmax){ float fog = exp(-uFog*sd); float pr = length(w - rd*sd); // A sphere smaller than a pixel must DIM, not alias: // widen the kernel out to the pixel and pay for it in // amplitude, which is what a real resolve does. float rr = max(gOrbR, 0.85*sd*gPX); float kk = exp(-pr*pr/(rr*rr))*(gOrbR*gOrbR)/(rr*rr); add += gLite*on*fog*kk*(5.0 + 13.0*uBright); float hr = gOrbR*6.0; add += gLite*on*fog*(0.26 + 0.42*uBright) * hr*hr/(pr*pr + hr*hr)/(1.0 + 0.20*sd); } // ---- the flock ---- if (doMoths > 0.5 && mothN > 0.5 && sd < 15.0){ for (int m = 0; m < 14; m++){ if (float(m) >= mothN) break; vec4 mh = hash42(lid*3.1 + float(m)*7.77 + 5.3); float ph = mh.w*6.2831; float sp = (0.55 + 1.25*mh.y)*(mh.z < 0.5 ? -1.0 : 1.0); float ang = TIME*sp + ph; // The radius BREATHES. A fixed radius is a ring, // and a ring is a planet and not a moth; letting it // swell and collapse is what makes the flock spiral // in at the light and fall away from it again. float rr0 = uMothR*(0.30 + 0.85*mh.x); float rad = rr0*(0.52 + 0.48*sin(TIME*(0.40 + 0.55*mh.y) + ph*2.0)); vec3 loc = vec3(rad*cos(ang), 0.26*rad*sin(ang*2.0 + ph), rad*sin(ang)); float tl = (mh.x - 0.5)*1.5; // the orbital plane, tilted float ct = cos(tl), st = sin(tl); loc = vec3(loc.x, loc.y*ct - loc.z*st, loc.y*st + loc.z*ct); loc += 0.07*uMothR*vec3(sin(TIME*13.0 + ph), sin(TIME*17.0 + ph*1.7), sin(TIME*15.0 + ph*2.3)); vec3 mp = O + loc; vec3 mw = mp - ro; float md = dot(mw, rd); if (md > 0.06 && md < tmax){ vec3 mo = mw - rd*md; // the perpendicular miss float wb = 0.42 + 0.58*abs(sin(TIME*11.0 + ph*4.0)); float ms = (0.020 + 0.022*wb)*(0.75 + 0.55*mh.z); float mr = max(ms, 0.90*md*gPX); // A moth is NOT a ball of light. The splat is an // ellipse across the ray, wide along the wing and // narrow along the body, and the span opens and // shuts with the beat - which is the entire read at // this size, because nothing else about it resolves. float ox = dot(mo, ex)/(mr*(0.80 + 1.55*wb)); float oy = dot(mo, ey)/(mr*0.58); float cov = clamp(1.0 - (ox*ox + oy*oy), 0.0, 1.0); cov = cov*cov*clamp((ms*ms)/(mr*mr), 0.0, 1.0); float li = 1.0/(1.0 + 3.2*dot(loc, loc)); vec3 mc = (gLite*(0.45 + 1.25*li)*wb*on + vec3(0.34, 0.29, 0.22)*0.16) * exp(-uFog*md)*(0.30 + 0.52*uBright); // ONE premultiplied layer, accumulated so that // the result does not depend on the order the // lattice happens to be walked in. That is what // lets a moth be a bright mote against the dark // nave AND a silhouette across the orb without // ever being depth sorted. mCol += mc*cov; mA = mA + cov - mA*cov; } } } } } } } return add; } // --------------------------------------------------------------- // the water // --------------------------------------------------------------- // Four wave trains, rotated off each other by an angle incommensurate with a // right angle, each with its phase warped by value noise so no train is // straight, and each moving at a speed proportional to one over the square // root of its wavenumber. // // That last part is gravity-wave DISPERSION and it is the whole reason this // does not read as a scrolling texture: in real water the long swell outruns // the short chop, the crossings drift, and the pattern never repeats its own // motion. Scroll every octave at one speed and the eye finds the loop // immediately, however many octaves there are. float rippleHi(vec2 p, float t){ mat2 R = mat2(0.8763, 0.4818, -0.4818, 0.8763); float h = 0.0, a = 1.0, f = 1.0, s = 0.0; for (int i = 0; i < 4; i++){ vec2 q = p*f; float ph = 6.2831*vnoise(q*0.33 + float(i)*7.1 + t*0.04); h += a*sin(q.x + 0.55*q.y + ph + t*2.6*inversesqrt(f)); s += a; p = R*p; a *= 0.52; f *= 2.15; } return h/s; } // two octaves, for the caustic Laplacian and for anything far enough away that // the top two octaves are already below a pixel float rippleLo(vec2 p, float t){ mat2 R = mat2(0.8763, 0.4818, -0.4818, 0.8763); float h = 0.0, a = 1.0, f = 1.0, s = 0.0; for (int i = 0; i < 2; i++){ vec2 q = p*f; float ph = 6.2831*vnoise(q*0.33 + float(i)*7.1 + t*0.04); h += a*sin(q.x + 0.55*q.y + ph + t*2.6*inversesqrt(f)); s += a; p = R*p; a *= 0.52; f *= 2.15; } return h/s; } // A wake radiating from the camera and decaying, so that wading disturbs the // surface it is wading through. Without it the water is a beautiful field the // viewer is not in. float wake(vec2 xz){ float r = length(xz - gCam); return sin(r*10.5 - gWT*6.0)*exp(-r*0.70)*smoothstep(0.04, 0.85, r); } float waterHeight(vec2 xz){ return gWL + uRipple*(0.0130*rippleHi(xz*3.0, gWT) + 0.0075*wake(xz)*clamp(uSpeed*0.4, 0.0, 1.2)); } // CAUSTICS ARE NOT A TEXTURE. They are the Jacobian of the refracted map: for // a shallow height field the bundle of rays through a patch of surface is // compressed by 1/(1 + d*div), where div is the Laplacian of the height and d // is how far the light has travelled since. So they spread and soften with // depth, tighten under a crest, and move WITH the water instead of under it. // // The sample spacing widens with depth too, which is the same statement made // twice: deep water cannot carry the fine detail of the surface above it. float caustic(vec2 xz, float depth){ if (uCaustic < 0.005) return 1.0; float e = clamp(0.07 + 0.30*depth, 0.07, 0.55); float h0 = rippleLo(xz*3.0, gWT); float lap = rippleLo((xz + vec2(e, 0.0))*3.0, gWT) + rippleLo((xz - vec2(e, 0.0))*3.0, gWT) + rippleLo((xz + vec2(0.0, e))*3.0, gWT) + rippleLo((xz - vec2(0.0, e))*3.0, gWT) - 4.0*h0; float foc = -lap/(e*e); // positive where the surface focuses float c = clamp(1.0 + 0.055*uRipple*(0.30 + depth)*foc, 0.04, 3.4); c = c*c; // filaments, not gradients return mix(1.0, c, clamp(uCaustic, 0.0, 2.0)); } // --------------------------------------------------------------- // surface shading. qual 1 = full (AO, shadow, fine detail), 0 = the cheap // version used inside the reflection and refraction marches. // --------------------------------------------------------------- vec3 shade(vec3 p, vec3 rd, vec3 n, float mat, float foot, float qual){ float det = mip(0.008, foot); float detT = mip(0.030, foot); float detM = mip(0.45, foot); float wh = waterHeight(p.xz); float above = p.y - wh; // signed height over the water float sub = smoothstep(0.012, -0.012, above); vec3 alb = gTile; float gloss = 0.0; vec2 tid, tf; if (mat < 1.5){ // ---- the flooded floor, in hexagonal mosaic ---- float T = HFLOR*uTileScale; float gm = hexJoint(p.xz, T, foot, tid, tf); vec4 th = hash42(tid + 4.3); // A mosaic is never one colour. Every tessera is fired slightly apart // from its neighbours, and the SHEETS they were laid in drift as well, // which is the difference between mosaic and a hexagon pattern. float sheet = fbm(p.xz*0.85 + 3.0); alb = gTile*(0.84 + 0.24*th.x)*(0.92 + 0.16*sheet); alb = mix(alb, gAcc, smoothstep(0.60, 0.80, sheet)*0.60*uAccent*detM); alb *= 1.0 + 0.10*(vnoise(p.xz*40.0) - 0.5)*det; alb *= mix(1.0, 0.45, step(0.985, th.z)*detT); // the odd dead tessera alb = mix(alb, gGrout*(0.85 + 0.30*th.y), gm); // silt collecting in the joints and in the corners of the plan float silt = fbm(p.xz*0.55 + 9.0); alb *= mix(1.0, 0.72 + 0.36*silt, uGrime*detM); alb = mix(alb, alb*vec3(0.72, 0.78, 0.70), gm*uGrime*0.55*detT); gloss = 0.40; } else if (mat < 2.5){ // ---- THE VAULT ---- // Coursed ashlar, laid in the vault's OWN coordinates: arc length up // the curve against distance along the barrel's axis. Course the web // in world x and z instead and the stones run diagonally across a // surface that is visibly curved, which is the single thing that makes // a procedural vault look printed on. float rib, boss; vaultField(p, rib, boss); vec2 q = mod(p.xz, gBay) - 0.5*gBay; float tw = p.y - gYs; float t = max(tw, 0.0); float Rr = gRb + gOff; float fa = barrel(q.x, t), fb = barrel(q.y, t); float useA = step(fb, fa); // which barrel is the surface float across = mix(q.y, q.x, useA); float along = mix(q.x, q.y, useA); float ang = atan(t, max(abs(across) + gOff, 1e-4)); // zero at the springing vec2 uvv = vec2(along + 0.41*step(0.0, across), Rr*ang); float ch = ASH_H*mix(1.0, uTileScale, 0.45); float cwid = ASH_L*mix(1.0, uTileScale, 0.45); vec2 sid; float jm = ashlar(uvv, ch, cwid, foot, sid); vec4 sh4 = hash42(sid + 21.9); alb = gStone*(0.74 + 0.26*sh4.x); alb *= 1.0 + 0.10*(vnoise(uvv*17.0) - 0.5)*mip(0.045, foot) + 0.06*(vnoise(uvv*90.0) - 0.5)*mip(0.010, foot); alb = mix(alb, gStone*vec3(0.40, 0.38, 0.35), jm); // dressed stone is a different stone: the ribs and the boss are cut // from a harder bed and they have been touched by more hands float ribn = 1.0 - smoothstep(-0.010, 0.060, rib); float bossn = 1.0 - smoothstep(-0.010, 0.050, boss); float dress = max(ribn, bossn); vec3 ribCol = gStone*vec3(1.22, 1.15, 1.03)*(0.92 + 0.14*hash12(floor(uvv*3.0))); alb = mix(alb, ribCol, dress*0.92); // a chamfer catch along the rib's own edge alb *= 1.0 + 0.28*smoothstep(0.075, 0.030, rib)*(1.0 - ribn)*detT; // soot and damp coming down out of the high vault, which is what stops // a stone ceiling reading as one flat grey float grimeF = fbm(p.xz*0.42 + vec2(4.0, 11.0)); alb *= mix(1.0, 0.62 + 0.52*grimeF, uGrime*0.85*detM); float ring = smoothstep(0.55, 0.84, fbm(uvv*0.55 + sh4.yz*11.0))*uDamp*detM; alb = mix(alb, vec3(0.30, 0.28, 0.23), ring*0.55*(1.0 - dress)); gloss = 0.12 + 0.22*dress; } else { // ---- wall, pier or ledge: glazed hexagonal mosaic ---- float T = HWALL*uTileScale; vec2 uv; float top = step(0.55, abs(n.y)); if (top > 0.5) uv = p.xz; // wall head or ledge top else if (abs(n.x) > 0.5) uv = vec2(p.z, p.y); else uv = vec2(p.x, p.y); if (top > 0.5) T = HFLOR*uTileScale; // bigger tesserae on the deck float gm = hexJoint(uv, T, foot, tid, tf); vec4 th = hash42(tid + 17.1); float sheet = fbm(uv*0.90 + 6.0); alb = gTile*(0.86 + 0.20*th.x)*(0.94 + 0.13*sheet); // the tooth of the glaze: too fine to resolve past a couple of metres, // but it is what stops a near wall reading as flat paint alb *= 1.0 + 0.13*(vnoise(uv*220.0) - 0.5)*mip(0.010, foot) + 0.07*(vnoise(uv*55.0) - 0.5)*mip(0.050, foot); // the odd tessera crazed or gone alb *= mix(1.0, 0.52, step(0.986, th.z)*detT); // Bands of accent tesserae drifting through the field, and a course of // them at the waterline, which every pool built before 1995 has. Keyed // to the STILL level rather than the live one - the mosaicist did not // know about the ripple. if (uAccent > 0.01){ alb = mix(alb, gAcc*(0.85 + 0.30*th.y), smoothstep(0.62, 0.82, sheet)*0.55*uAccent*detM); if (top < 0.5){ float ah = gWL + 0.14; float cov = band(uv.y - ah, 1.30*T, foot); alb = mix(alb, gAcc*(0.88 + 0.26*th.w), cov*uAccent); } } alb = mix(alb, gGrout*(0.88 + 0.24*th.y), gm); // grime rising out of the water, and damp coming down the wall float gr = fbm(uv*1.25 + 7.0); if (top < 0.5){ float tide = smoothstep(0.45, 0.0, abs(above - 0.05)); alb *= mix(1.0, 0.66 + 0.42*gr, (smoothstep(1.1, 0.0, above)*0.5 + tide*0.6)*uGrime*detM); alb = mix(alb, alb*vec3(0.62, 0.66, 0.60), smoothstep(0.52, 1.02, gr)*uDamp*0.6*detM); } gloss = 0.58; } // Everything the water touches is WET, and wet is not a colour change, it // is a darkening plus a gloss. The band tracks the live ripple height, so // the wet line breathes with the surface instead of ruling a straight line // across the tile. float wet = sub + (1.0 - sub)*exp(-max(above, 0.0)*11.0); alb *= 1.0 - 0.28*wet; gloss = mix(gloss, 0.95, wet); // ---- light ---- float ao = 1.0; if (qual > 0.5) ao = mix(1.0, calcAO(p, n), uAO); vec3 kd; float kdist; float lit = lighting(p, n, kd, kdist); float sh = 1.0; if (qual > 0.5 && uShadow > 0.01) sh = mix(1.0, shadowRay(p + n*0.06, kd, kdist - 0.20), uShadow); // Caustics, from the same field, on both sides of the surface. // // Underwater it is the light arriving through the ripple; ABOVE water it // is the same pattern bounced back up off it, attenuated by height. The // reach is deliberately long here: the whole point of putting a vault over // a pool is that the water writes on it, forty feet up. float depth = max(wh - p.y, 0.0); float cw = 1.0; if (sub > 0.5){ cw = caustic(p.xz, depth)*(0.55 + 0.45*max(n.y, 0.0)); } else { float h = max(above, 0.0); float reach = exp(-h*0.16)*(0.30 + 0.70*(1.0 - max(n.y, 0.0)) + 0.55*max(-n.y, 0.0)); cw = mix(1.0, caustic(p.xz, 0.35 + h*0.35), clamp(reach, 0.0, 1.0)*0.85); } vec3 diff = gLite*lit*(0.52 + 0.48*sh)*(2.4 + 3.8*uBright)*cw; // no GI, but a pale stone room is almost entirely bounce: a hemispheric // fill, plus an explicit term for light coming back off the water, which // is what puts any brightness on the vault at all float dn = 0.5 - 0.5*n.y; float zdim = mix(1.0, 0.25 + 1.05*lightZone(p.xz), uZones); vec3 amb = ao*zdim*(mix(gTile, gAmb, 0.5)*(0.075 + 0.095*uBright) + gShal*gAmb*(0.24 + 0.34*uBright)*dn*mix(1.0, cw, 0.5)); vec3 col = alb*(diff*ao*0.85 + amb); // and the glaze itself: a real highlight off the key direction, which is // most of what says "tile" rather than "plaster" if (uGloss > 0.01 && lit > 0.001){ vec3 hv = normalize(kd - rd); float sp = pow(max(dot(n, hv), 0.0), mix(24.0, 150.0, gloss)); col += gLite*sp*lit*sh*gloss*uGloss*(0.35 + 0.5*uBright); } return col; } // --------------------------------------------------------------- void main(){ vec2 fragCoord = gl_FragCoord.xy; vec2 R = RENDERSIZE.xy; float T = TIME; float t = T*uSpeed; gTile = pick4(vec3(0.89, 0.83, 0.72), vec3(0.90, 0.78, 0.59), vec3(0.70, 0.77, 0.86), vec3(0.83, 0.89, 0.74), uPalette); gGrout = pick4(vec3(0.58, 0.52, 0.44), vec3(0.57, 0.47, 0.35), vec3(0.42, 0.48, 0.57), vec3(0.53, 0.59, 0.47), uPalette); gAcc = pick4(vec3(0.45, 0.27, 0.13), vec3(0.76, 0.55, 0.18), vec3(0.05, 0.15, 0.31), vec3(0.15, 0.40, 0.26), uPalette); gStone = pick4(vec3(0.85, 0.78, 0.66), vec3(0.86, 0.73, 0.54), vec3(0.63, 0.69, 0.78), vec3(0.75, 0.80, 0.67), uPalette); gShal = pick4(vec3(0.28, 0.74, 0.80), vec3(0.38, 0.70, 0.60), vec3(0.24, 0.54, 0.80), vec3(0.46, 0.84, 0.60), uPalette); gDeep = pick4(vec3(0.02, 0.17, 0.29), vec3(0.05, 0.19, 0.21), vec3(0.02, 0.10, 0.26), vec3(0.05, 0.26, 0.18), uPalette); // The orbs are white light at a chosen temperature, so the palette does // not get a vote here - it dresses the stone and the water and nothing else. gLite = kelvin(uTemp); // BOUNCE IS NOT LAMP COLOUR. Tint the fill with the same amber as the orbs // and the whole room collapses to one hue - which is the failure mode of // every "warm" grade. Second and third bounces have crossed pale stone and // a metre of blue-green water, so the fill is pulled most of the way back // to neutral and the water is left to supply the cold end of the frame. gAmb = mix(vec3(1.0), gLite, 0.40); // ---- the vault lattice ---- // The bay is a WHOLE number of plan cells, so the groins and the transverse // arches come down over the arcade rather than through the middle of it. float nb = max(1.0, floor(uBay + 0.5)); gBay = uCell*nb; gRb = 0.5*gBay; gOff = gRb*uPoint; gYs = uHeight; gTc = sqrt(max((gRb + gOff)*(gRb + gOff) - gOff*gOff, 0.0)); gRibR = clamp(0.022*gBay, 0.060, 0.170); // The orbs hang on a whole-number subdivision of the bay, which is the // same trick the coffers used and for the same reason: an orb is then // never under a rib, never on a groin, and never straddling a bay edge. float nl = max(1.0, floor(gBay/max(uLightSpace, 0.5) + 0.5)); gLG = gBay/nl; gOrbR = uOrbSize; gWL = uWater; gWT = T*uFlow; // ---- the wade ---- float z = t*1.05; vec3 ro = vec3(pathX(z) + uBob*0.026*sin(t*2.4), 1.50 + uBob*0.034*sin(t*4.8), z); gCam = ro.xz; float dz = 0.35; vec3 fw = normalize(vec3(pathX(z + dz) - pathX(z - dz), 0.0, 2.0*dz)); float yaw = uYaw + uBob*0.05*sin(t*0.79 + 1.0); float cy = cos(yaw), sy = sin(yaw); fw = normalize(vec3(fw.x*cy + fw.z*sy, 0.0, fw.z*cy - fw.x*sy)); vec3 rt = normalize(cross(fw, vec3(0.0, 1.0, 0.0))); vec3 up = cross(rt, fw); fw = normalize(fw + up*(uPitch + uBob*0.020*sin(t*1.6))); rt = normalize(cross(fw, vec3(0.0, 1.0, 0.0))); up = cross(rt, fw); float roll = uBob*0.013*sin(t*2.0 + 0.5); vec3 rr2 = rt*cos(roll) + up*sin(roll); up = up*cos(roll) - rt*sin(roll); rt = rr2; // ---- the lens, bent before anything is traced ---- vec2 uv = (fragCoord - 0.5*R)/R.y; float vhs = uVHS; float ln = hash11(floor(fragCoord.y) + floor(T*24.0)*17.0); uv.x += vhs*(0.0022*(ln - 0.5) + 0.0026*sin(T*1.7 + fragCoord.y*0.031)); uv *= 1.0 + vhs*0.040*dot(uv, uv); vec3 rd = normalize(uv.x*rt + uv.y*up + uLens*fw); gPX = 1.0/(uLens*R.y); // ---- where the water is, BEFORE the march ---- // // Solved analytically. The surface is never marched and never appears in // map(): its displacement is 13 mm, which is under a pixel at any distance // past arm's length, so a marcher would spend a hundred steps resolving // something nobody can see. One plane hit plus one Newton step against the // real height puts the intersection where it belongs, and everything the // viewer actually reacts to is in the NORMAL. float tW = 1e9; if (rd.y < -0.004 && ro.y > gWL){ float tp = (gWL - ro.y)/rd.y; if (tp > 0.0 && tp < FAR){ vec3 pw = ro + rd*tp; // a crest is reached sooner than the still plane, a trough later; // clamped because at grazing incidence the correction diverges tp += clamp((waterHeight(pw.xz) - pw.y)/rd.y, -0.45, 0.45); tW = max(tp, 0.0); } } float tMax = min(FAR, tW); // ---- march the building, but only as far as the water ---- float tt = 0.0, tHit = tMax, mat = 0.0; for (int i = 0; i < 144; i++){ vec2 m = map(ro + rd*tt); // The hit test opens up with distance or a grazing ray down a 40 m // nave runs out of steps - but not too far: stopping short by e along // the ray is a slide of e/cos ALONG a grazing surface, which bands // every lighting term computed from the hit point. Hence a tight-ish // epsilon and the Newton step below rather than one loose epsilon // doing both jobs. if (m.x < 0.0024*tt + 0.0009){ tHit = tt; mat = m.y; break; } tt += m.x*0.90; if (tt > tMax) break; } float zf = lightZone((ro + rd*18.0).xz); vec3 fogCol = mix(gStone, gAmb, 0.45)*(0.050 + 0.105*uBright) * mix(1.0, 0.10 + 1.50*zf, uZones); vec3 col = fogCol; float tShow = FAR; if (mat > 0.5){ vec3 p = ro + rd*tHit; vec3 n = calcNormal(p); // One Newton step onto the true intersection: moving dt along the ray // changes the signed distance by dt*dot(n, rd), so the residual maps // straight onto a correction in t. Exact for a plane, one extra map(), // and it is what removes the contour banding on oblique walls. float ndv0 = min(dot(n, rd), -0.06); tHit += clamp(-map(p).x/ndv0, 0.0, 0.75); p = ro + rd*tHit; n = calcNormal(p); float ndv = dot(n, rd); // The grazing term is sqrt(c*c + k) and not max(|c|, k): a hard clamp // has an EDGE where it engages, and on a flat wall that edge is a cone // about the normal - a ring painted across the surface. float foot = tHit*gPX/sqrt(ndv*ndv + 0.004); col = shade(p, rd, n, mat, foot, 1.0); tShow = tHit; } else if (tW < FAR){ // ---------------- the surface ---------------- vec3 pw = ro + rd*tW; float foot = tW*gPX/max(-rd.y, 0.02); // An unresolved ripple is not a flat surface, it is a ROUGH one. // // Fading the normal to vertical with distance is right - it is the // only way to stop the far water aliasing into crawling sparkle - but // done alone it turns the far end of the nave into a mirror, which is // worse than the aliasing. So the same term that flattens the geometry // widens the specular lobe by the same amount: the shimmer stops being // shape and becomes gloss, which is exactly what it is. float res = mip(0.28, foot); float e = max(0.012, 0.55*foot); float h = waterHeight(pw.xz); float hx = waterHeight(pw.xz + vec2(e, 0.0)); float hz = waterHeight(pw.xz + vec2(0.0, e)); vec3 n = normalize(vec3((h - hx)*res, e, (h - hz)*res)); float ndv = max(dot(-rd, n), 0.0); // Schlick, with the 0.02 of water and a floor under it so the surface // never goes perfectly non-reflective head-on float F = 0.02 + 0.98*pow(1.0 - ndv, 5.0); F = clamp(F, 0.02, 0.96); // ---- reflection ---- vec3 rr = reflect(rd, n); vec3 refl = fogCol; float rt3 = 0.05, rHit = -1.0, rMat = 0.0; for (int i = 0; i < 56; i++){ vec2 m = map(pw + rr*rt3); if (m.x < 0.004*rt3 + 0.002){ rHit = rt3; rMat = m.y; break; } rt3 += m.x*0.92; if (rt3 > 32.0) break; } if (rMat > 0.5){ vec3 rp = pw + rr*rHit; vec3 rn = calcNormal(rp); float rfoot = (tW + rHit)*gPX/sqrt(dot(rn, rr)*dot(rn, rr) + 0.02); refl = shade(rp, rr, rn, rMat, rfoot, 0.0); refl = mix(refl, fogCol, 1.0 - exp(-uFog*rHit)); } // The orbs in the water. They are not geometry, so they are not in the // reflection march either - they have to be splatted onto the // reflected ray by hand, and they are far too much of the picture to // leave out: a hall of floating lights over still water is mostly the // half of it that is upside down. vec3 dmy; float dma; refl += orbRay(pw, rr, rMat > 0.5 ? rHit : 32.0, 0.0, dmy, dma); // ---- refraction ---- vec3 rf = refract(rd, n, 0.7502); // 1 / 1.333 vec3 tran = fogCol; float ft2 = 0.03, fHit = 6.0, fMat = 0.0; for (int i = 0; i < 40; i++){ vec2 m = map(pw + rf*ft2); if (m.x < 0.004*ft2 + 0.002){ fHit = ft2; fMat = m.y; break; } ft2 += m.x*0.92; if (ft2 > 12.0) break; } if (fMat > 0.5){ vec3 fp = pw + rf*fHit; vec3 fn = calcNormal(fp); float ffoot = (tW + fHit)*gPX/sqrt(dot(fn, rf)*dot(fn, rf) + 0.02); tran = shade(fp, rf, fn, fMat, ffoot, 0.0); } // Beer-Lambert down the refracted path, plus what the water itself // scatters back. This is the entire reason shallow water over pale // mosaic is turquoise and the same water two metres deeper is not: red // is gone within a metre, and once the transmitted light is dark // enough the in-scattered colour is ALL there is. float sig = mix(1.20, 0.18, uClarity); vec3 absorb = exp(-sig*vec3(1.20, 0.29, 0.22)*min(fHit, 12.0)); // Held up deliberately against the grade. Everything ABOVE the surface // is amber now, so the one term in the frame that is neither lamp nor // bounce - the light the water itself scatters back - is the only cold // the picture has, and it has to be loud enough to survive the print. vec3 inSc = mix(gDeep, gShal, exp(-0.55*fHit))*(0.82 + 1.05*uBright) * mix(1.0, 0.15 + 1.5*lightZone(pw.xz), uZones); tran = tran*absorb + inSc*(1.0 - absorb); col = mix(tran, refl, F); // The glint: an orb seen in the surface at a grazing angle, smeared // along the ripple. Roughness comes back in here, so the far water // that lost its normal keeps a long soft streak instead of a mirror. vec3 kd; float kdist; float lit = lighting(pw + vec3(0.0, 0.01, 0.0), n, kd, kdist); vec3 hv = normalize(kd - rd); float sp = pow(max(dot(n, hv), 0.0), mix(60.0, 900.0, res)); col += gLite*sp*lit*(0.5 + 0.6*uBright)*(0.35 + 0.65*F); tShow = tW; } if (tShow < FAR){ float fg = 1.0 - exp(-uFog*tShow); // The fade has to COMPLETE before rays start missing, not at the far // plane itself: otherwise a hit at 46 m keeps 15 % of its surface // while the ray beside it, which reached 48 and missed, keeps none - // and that 15 % is a hard stripe at the vanishing point. col = mix(col, fogCol, max(fg, smoothstep(FAR*0.55, FAR*0.92, tShow))); } // ---- the orbs, and the moths around them ---- vec3 mCol; float mA; col += orbRay(ro, rd, min(tShow, FAR), 1.0, mCol, mA); // premultiplied, so the wings darken what is behind them and glow at the // same time - which is what a moth in front of a lamp actually does col = col*(1.0 - mA*0.55) + mCol; // ---- in-scattering: the shafts, and the humidity ---- if (uHaze > 0.005){ float tm = min(tShow, FAR); float jt = hash12(fragCoord + floor(T*30.0)); float glow = 0.0; for (int i = 0; i < 18; i++){ // Sample positions depend ONLY on i, never on where the ray // landed. Spacing them tm/18 apart ties the whole integral to // tHit, which jumps several metres between the ray that reaches // the far plane and its neighbour that clips a wall - a hard // vertical stripe at the vanishing point. Quadratic in i: fixed in // world space, dense near the camera where the halo needs the // resolution, coarse out where the fog has already eaten it. float u2 = (float(i) + jt)/18.0; float s = FAR*u2*u2; if (s > tm) break; float ds = FAR*2.0*u2/18.0; ds *= min(1.0, (tm - s)/max(ds, 1e-3)); vec3 q = ro + rd*s; // Nearest orb by FOLDING the lattice, and the local DENSITY of // working orbs rather than whether one particular orb is lit: // floor() plus orbOn() are both piecewise constant, so the glow // would step at every lattice plane - and the camera stands ON the // plane x = 0, which projects to a straight full-height line // through the vanishing point. The per-orb height bob is dropped // here for exactly the same reason. vec2 f = mod(q.xz, gLG) - 0.5*gLG; float dl = length(vec3(f.x, uOrbY - q.y, f.y)); float on = uLightOn*mix(1.0, 0.05 + 1.90*lightZone(q.xz), uZones); // and the air above water is thicker than the air above a carpet float humid = 1.0 + 0.9*exp(-max(q.y - gWL, 0.0)*0.8); glow += on*humid*ds*exp(-uFog*2.0*s)/(1.0 + 2.4*dl*dl); } col += gLite*glow*uHaze*0.34; } // ---- the grade, and then it is put on tape ---- vec2 sc = fragCoord/R; col = warmGrade(col, uWarm); col = mix(col/(1.0 + col*0.80), filmic(col), uFilm); // a toe under the curve's shoulder: the blacks come off the floor and the // midtones separate, which is the half of "filmic" that is not the roll-off col = mix(col, col*col*(3.0 - 2.0*col), 0.32*uFilm); // and the print itself - amber through the midtones, a trace of cyan left // in the deep shadow, which is most of what the word means to the eye float gy = dot(col, vec3(0.2126, 0.7152, 0.0722)); float sha = (1.0 - gy)*(1.0 - gy); col = mix(col, col*vec3(1.040, 1.000, 0.945) + vec3(0.0, 0.012, 0.038)*sha, clamp(uWarm, 0.0, 1.5)*0.85); // and the shadows lean cold rather than merely dark. A warm grade with warm // shadows is a sepia print; the cold end has to live SOMEWHERE, and the // deepest quarter of the frame is where film has always put it. col = mix(col, col*vec3(0.90, 0.98, 1.14), clamp(uWarm, 0.0, 1.5)*0.42*sha); col *= 1.0 - uScan*vhs*0.18*(0.5 + 0.5*sin(fragCoord.y*PI)); float nz = hash12(fragCoord + floor(T*24.0)*vec2(13.7, 91.3)); float hb = smoothstep(0.024, 0.0, sc.y); col = mix(col, vec3(nz)*0.5 + col*0.45, hb*vhs*0.6); float drow = hash11(floor(sc.y*180.0) + floor(T*12.0)*7.3); float dseg = step(0.997, hash11(floor(sc.x*40.0)*3.1 + floor(sc.y*180.0)*11.7 + floor(T*12.0)*29.0)); col += vhs*0.16*dseg*step(0.990, drow)*vec3(1.0, 0.99, 0.95); col += (nz - 0.5)*uGrain*0.12; float r2 = dot(uv, uv); col.r *= 1.0 - vhs*0.08*r2; col.b *= 1.0 - vhs*0.13*r2; col *= 1.0 - uVignette*1.25*dot(sc - 0.5, sc - 0.5); float lum = dot(col, vec3(0.299, 0.587, 0.114)); col = mix(vec3(lum), col, 1.28 + 0.14*uWarm - vhs*0.20); // the curve greys it; put it back col = col*(1.0 - vhs*0.05) + vhs*0.018; // lifted black // Half an LSB before the 8-bit write. A pale stone room is almost entirely // smooth low-amplitude gradients, which is exactly where quantisation // shows up as contours across a vault - and the grain slider cannot be // relied on for this, because a viewer is entitled to zero it. col += (hash12(fragCoord + 0.5) - 0.5)/255.0; gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0); }
THE CATHEDRAL POOLROOMS. An endless flooded cathedral in ankle-deep water, generated rather than modelled: no level file and no mesh, every pier and every arcade opening decided by hashing an integer cell index, so the building is unbounded and identical for everyone who runs it. The plan is still the backrooms trick - a 2D box SDF over the 3x3 cells around the sample, extruded from the floor to the impost line - but the lid is gone. THE CEILING IS THE WORK. A drop ceiling is a plane and costs nothing; a vault is a shape, and the only vault that can tile an infinite building is one written as a single folded cell, so p.xz is folded into one bay and the vault is drawn ONCE, everywhere. Two things fall straight out of that: no arch can ever collide with a neighbour, because there is only ever one arch; and the cost of an infinite cathedral is the cost of one bay. The cell is a groin vault, which is the UNION of two barrel vaults and not their intersection - the intersection is a cloister vault, it closes to a point at the crown, and that is a dome and not a cathedral. Each barrel is a POINTED arch, built as a circle whose centre is pushed outward past the springing by uPoint, so the two arcs meet at an angle instead of a tangent; the crown then rises as sqrt(1 + 2*uPoint) times the half-bay, which is the entire reason the pointed arch was invented and the reason the ceiling here stands three times the height of the walls carrying it. It is not a height field. The obvious way to arch a ceiling is to make its height a function of x and z and subtract that, and such a profile is VERTICAL at the springing, so its gradient is unbounded exactly where the geometry is most delicate and a sphere tracer walks straight through the haunch. A barrel written as a radius about an axis has gradient one everywhere. Folding has one failure and it is instructive: below the springing line the barrel field degenerates to a slab, and the max of two slabs is a square shaft that would wall off every bay at floor level. So the void is unioned with the whole half-space beneath the springing - which is exact rather than a fudge, because every scrap of vault masonry lives above that plane - and the union leaves precisely one zero, at the corner of each bay at impost height. That zero is not an artefact to be smoothed away, it is where the vault lands: put a corbel there and the field becomes honest. The ribs are subtracted tubes whose axes are curves the surfaces already share - the groin where the two barrels cross, the transverse arch in the plane of the bay edge, the ridge along each crown, a boss at their crossing, and a pair of intermediate liernes that turn a quadripartite vault into a net vault as uRibs is raised. The webbing between them is coursed ashlar laid in the vault's OWN coordinates, arc length up the curve against distance along the axis, so the courses run with the shape instead of across it. The lights are floating orbs and they are deliberately NOT in the distance field. An emissive sphere in map() is found by every shadow ray and every ambient-occlusion probe, and a light that occludes itself blacks out the room it is lighting; so each orb is an analytic splat along the ray - one dot product for depth, one for the perpendicular miss - and a point in the light gather, and nothing else. Their colour is a blackbody curve in kelvin, normalised so that changing the temperature changes the colour and not the exposure, from 1700 K candle to 7500 K overcast. Around each orb is a flock of moths. Each moth is on a tilted, precessing orbit whose radius breathes, so the flock spirals toward the light and falls away from it rather than sitting on a ring; they are projected onto the ray and accumulated as ONE order-independent premultiplied layer, which is what lets a moth be a bright mote against the dark nave and a dark silhouette across the orb without ever being sorted. The water is unchanged and still the subject, and still not marched: a pool ripple is 8 mm, under a pixel past arm's length, so the surface is an analytic plane hit plus one Newton step and everything anyone sees is in the NORMAL. Four rotated wave trains, phase-warped by value noise, each moving at one over the square root of its wavenumber - gravity-wave dispersion - so the long trains outrun the short ones and the field never reads as a scrolling texture. Caustics are the focusing term of the refracted map, one over one plus depth times the Laplacian of the ripple, and the same field attenuated by height is what puts moving light forty feet up on the vault webbing. The picture is graded warm and graded in LINEAR, which is the only place a grade is a multiply rather than a wish: the gain is warmed and the lift is cooled BEFORE the tone curve, and the curve itself is a fit to the ACES output transform rather than the Reinhard divide it replaces. Reinhard has neither a toe nor a shoulder, and the shoulder is the whole reason a light reads as a light instead of a white disc with a hard rim - which matters more here than anywhere, because the subject of the room is forty floating lights. Warm orbs over cold water is also the oldest trick in colour film, and it is free: the orbs are tungsten by default and the water keeps the blue-green it gets from Beer-Lambert, so the two ends of the frame separate without a single hand-placed colour. Every tile in the building is a HEXAGON: a two-sublattice fold, a hexagonal norm for the joint distance, and the same box filter as before, decaying to the mosaic's duty cycle rather than to white, because a periodic grid seen edge-on averages to grey. Forty-seven controls: walk, lens, yaw and pitch, wander and turn, sway, cell size, wall density, closed chapels, arcade frequency and width, impost height, vault bay, arch point, rib density, ledge, water depth, ripple, flow, caustics, clarity, orb spacing, working orbs, orb height, orb size, colour temperature, brightness, dark zones, moths, swarm radius, haze, fog, four palettes, filmic curve, print warmth, mosaic scale, accent course, grime, damp, ambient occlusion, shadow, gloss, and four camcorder controls.
export clip · render a video in your browser
Renders to video in your browser (no upload), stepping the real renderer frame by frame so multipass + feedback come out right. motion auto-animates the shader's own knobs — each export rolls a fresh take (🎲 re-roll, 🔒 to pin one you like). GIF is slower with a tighter length cap; MP4/WebM are faster. Some devices record in real time (noted below if so) — keep the tab visible.
embed · drop this shader into any page
Renders client-side in the reader's browser (CC0 — no attribution required, but the backlink is appreciated).