/*{ "DESCRIPTION": "A procedurally generated pack of cigarettes turning on a PlayStation-1 'examine item' screen. It is not a raymarcher wearing a pixel filter: it is a small software rasteriser, and every PS1 artefact comes from doing what the PS1 did. Vertices are projected onto a 240-line screen and ROUNDED to whole pixels, as the GTE handed the GPU integer coordinates, so every corner jumps pixel to pixel as the pack turns and the edges wobble. Each quad is two triangles filled from those snapped corners, with barycentrics taken in screen space and used as they are - no divide by depth - so the textures swim and kink along each quad's diagonal exactly as affine mapping did (the perspective-correct answer is computed too, and a dial blends to it). Texels are point sampled, light is per vertex and MODULATES the texture up to twice as bright, colour is 15-bit, and the ordered dither is the PS1's own 4x4 matrix, floor(bayer/2) - 4 in 8-bit steps. Back-face culling is free: the faces are wound so cross(B-A, C-A) is the outward normal, projection keeps the winding, and a negative screen area is a face turned away. An optional ordering-table mode sorts by one depth per polygon, as the hardware did, instead of per pixel. Each pack comes from one integer: size (king, 100s, compact), a palette, one of five wrap layouts, an emblem, a brand name built from syllables - or, one pack in eight, a house brand the syllables cannot reach - a variant word, a warning, sealed or opened, how many are left, which slots are empty and which cigarettes stand proud. The filters seen down the open lid ARE the count: the twenty slots are taken in the order (a*s + b) mod 20 with a coprime to 20, a slot is still full if its turn is under the count, and the ones standing proud are the next three due, found by inverting the permutation - so the caption, the holes and the raised cigarettes cannot disagree. All of those decisions are exact integer arithmetic carried in floats, every intermediate whole and under 2^24, and all text packs four 5-bit symbols to a float so every divide is by a power of two and exact. The wrap artwork is drawn in millimetres of the CLOSED pack, so the design runs unbroken across the lid cut until the lid swings. Controls: resolution, vertex snap, affine amount, dither, colour depth, texel density, spin, focal length, elevation, camera sway, seconds per pack, seed, lid, pulled cigarettes, key light, gloss, fog, floor, caption, ordering-table sort and exposure.", "CREDIT": "CC0", "CATEGORIES": ["ps1", "retro", "rasterizer", "procedural", "lowpoly", "3d", "dither", "generator"], "INPUTS": [ { "NAME": "uRes", "TYPE": "float", "DEFAULT": 240, "MIN": 120, "MAX": 480 }, { "NAME": "uJitter", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 4 }, { "NAME": "uAffine", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uDither", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uBits", "TYPE": "float", "DEFAULT": 5, "MIN": 3, "MAX": 8 }, { "NAME": "uTexel", "TYPE": "float", "DEFAULT": 1.25, "MIN": 0.4, "MAX": 3 }, { "NAME": "uSpin", "TYPE": "float", "DEFAULT": 0.90, "MIN": -3, "MAX": 3 }, { "NAME": "uLens", "TYPE": "float", "DEFAULT": 3.40, "MIN": 1.5, "MAX": 6 }, { "NAME": "uElev", "TYPE": "float", "DEFAULT": 0.30, "MIN": 0, "MAX": 0.9 }, { "NAME": "uSway", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uCycle", "TYPE": "float", "DEFAULT": 9, "MIN": 4, "MAX": 30 }, { "NAME": "uSeed", "TYPE": "float", "DEFAULT": 0, "MIN": 0, "MAX": 64 }, { "NAME": "uLid", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uCigs", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uLight", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 2 }, { "NAME": "uGloss", "TYPE": "float", "DEFAULT": 0.50, "MIN": 0, "MAX": 1 }, { "NAME": "uFog", "TYPE": "float", "DEFAULT": 0.60, "MIN": 0, "MAX": 1 }, { "NAME": "uFloor", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0, "MAX": 1 }, { "NAME": "uHud", "TYPE": "float", "DEFAULT": 1, "MIN": 0, "MAX": 1 }, { "NAME": "uSort", "TYPE": "float", "DEFAULT": 0, "MIN": 0, "MAX": 1 }, { "NAME": "uExpo", "TYPE": "float", "DEFAULT": 1.00, "MIN": 0.3, "MAX": 2 } ] }*/ #define PI 3.14159265359 #define TAU 6.28318530718 // ---------------------------------------------------------------- hashes // mod() near an exact multiple can floor the wrong way on a GPU that // divides by multiplying with a reciprocal; fix it up, never trust it. float pmod(float x, float p){ float r = x - p * floor(x / p); if (r < 0.0) r += p; if (r >= p) r -= p; return r; } // cubing is a permutation modulo 4091 (3 does not divide 4090) float icube(float x){ return pmod(pmod(x * x, 4091.0) * x, 4091.0); } float gBase; // per-pack key // a whole number 0..4090 for draw j of the current pack float th(float j){ float x = pmod(gBase + j * 557.0, 4091.0); x = icube(pmod(x * 293.0 + 1571.0, 4091.0)); return icube(pmod(x * 61.0 + 877.0, 4091.0)); } // field of a whole number: floor(h / d) mod m, with a half-step of // margin so the division cannot land a hair under an integer float fld(float h, float d, float m){ float q = floor((h + 0.5) / d); return q - m * floor((q + 0.5) / m); } // decorative grain only - never decides structure 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); } // ---------------------------------------------------------------- tables // TABLES BEGIN (generated by gen_tables.py - do not edit) // 5x7 glyph rows, top row first: .x = rows 0-3, .y = rows 4-6, // five bits a row, column 0 in the lowest bit. Generated by gen_tables.py. vec2 glyphRows(float g){ if (g == 1.0) return vec2(1033774.0, 17969.0); // A if (g == 2.0) return vec2(509487.0, 15921.0); // B if (g == 3.0) return vec2(34350.0, 14881.0); // C if (g == 4.0) return vec2(574759.0, 7473.0); // D if (g == 5.0) return vec2(492607.0, 31777.0); // E if (g == 6.0) return vec2(492607.0, 1057.0); // F if (g == 7.0) return vec2(951854.0, 31281.0); // G if (g == 8.0) return vec2(1033777.0, 17969.0); // H if (g == 9.0) return vec2(135310.0, 14468.0); // I if (g == 10.0) return vec2(270620.0, 6440.0); // J if (g == 11.0) return vec2(103729.0, 17701.0); // K if (g == 12.0) return vec2(33825.0, 31777.0); // L if (g == 13.0) return vec2(710513.0, 17969.0); // M if (g == 14.0) return vec2(708145.0, 17977.0); // N if (g == 15.0) return vec2(575022.0, 14897.0); // O if (g == 16.0) return vec2(509487.0, 1057.0); // P if (g == 17.0) return vec2(575022.0, 22837.0); // Q if (g == 18.0) return vec2(509487.0, 17701.0); // R if (g == 19.0) return vec2(459838.0, 15888.0); // S if (g == 20.0) return vec2(135327.0, 4228.0); // T if (g == 21.0) return vec2(575025.0, 14897.0); // U if (g == 22.0) return vec2(575025.0, 4433.0); // V if (g == 23.0) return vec2(706097.0, 10933.0); // W if (g == 24.0) return vec2(141873.0, 17962.0); // X if (g == 25.0) return vec2(141873.0, 4228.0); // Y if (g == 26.0) return vec2(139807.0, 31778.0); // Z if (g == 27.0) return vec2(0.0, 6336.0); // . if (g == 28.0) return vec2(458752.0, 0.0); // - if (g == 29.0) return vec2(2180.0, 0.0); // ' if (g == 30.0) return vec2(714286.0, 14899.0); // 0 if (g == 31.0) return vec2(135364.0, 14468.0); // 1 if (g == 32.0) return vec2(279086.0, 31812.0); // 2 if (g == 33.0) return vec2(266527.0, 14896.0); // 3 if (g == 34.0) return vec2(305544.0, 8479.0); // 4 if (g == 35.0) return vec2(539711.0, 14896.0); // 5 if (g == 36.0) return vec2(492620.0, 14897.0); // 6 if (g == 37.0) return vec2(139807.0, 2114.0); // 7 if (g == 38.0) return vec2(476718.0, 14897.0); // 8 if (g == 39.0) return vec2(1001006.0, 6416.0); // 9 return vec2(0.0); } // Strings, 16 symbols per vec4 (4 per float). ids 0-7 warning lines, // 8-27 variant words (two per palette), 28-29 and 30-31 caption lines. vec4 strRow(float id){ if (id == 0.0) return vec4(376243.0, 7625.0, 0.0, 0.0); // "SMOKING" if (id == 1.0) return vec4(405803.0, 19.0, 0.0, 0.0); // "KILLS" if (id == 2.0) return vec4(376243.0, 7625.0, 0.0, 0.0); // "SMOKING" if (id == 3.0) return vec4(444456.0, 19.0, 0.0, 0.0); // "HARMS" if (id == 4.0) return vec4(665265.0, 0.0, 0.0, 0.0); // "QUIT" if (id == 5.0) return vec4(24046.0, 0.0, 0.0, 0.0); // "NOW" if (id == 6.0) return vec4(35316.0, 15459.0, 0.0, 0.0); // "TOBACCO" if (id == 7.0) return vec4(405803.0, 19.0, 0.0, 0.0); // "KILLS" if (id == 8.0) return vec4(406182.0, 45248.0, 18934.0, 0.0); // "FULL FLAVOR" if (id == 9.0) return vec4(4274.0, 0.0, 0.0, 0.0); // "RED" if (id == 10.0) return vec4(143847.0, 0.0, 0.0, 0.0); // "GOLD" if (id == 11.0) return vec4(269612.0, 628.0, 0.0, 0.0); // "LIGHTS" if (id == 12.0) return vec4(669869.0, 12776.0, 0.0, 0.0); // "MENTHOL" if (id == 13.0) return vec4(628294.0, 308232.0, 654.0, 0.0); // "FRESH MINT" if (id == 14.0) return vec4(185730.0, 0.0, 0.0, 0.0); // "BLUE" if (id == 15.0) return vec4(269612.0, 628.0, 0.0, 0.0); // "LIGHTS" if (id == 16.0) return vec4(103955.0, 12329.0, 0.0, 0.0); // "SPECIAL" if (id == 17.0) return vec4(99714.0, 11.0, 0.0, 0.0); // "BLACK" if (id == 18.0) return vec4(667942.0, 20037.0, 0.0, 0.0); // "FILTERS" if (id == 19.0) return vec4(244011.0, 19.0, 0.0, 0.0); // "KINGS" if (id == 20.0) return vec4(239183.0, 394697.0, 0.0, 0.0); // "ORIGINAL" if (id == 21.0) return vec4(379572.0, 8809.0, 0.0, 0.0); // "TURKISH" if (id == 22.0) return vec4(180915.0, 307826.0, 621.0, 0.0); // "SUPERSLIMS" if (id == 23.0) return vec4(58866.0, 620.0, 0.0, 0.0); // "ROYALS" if (id == 24.0) return vec4(667942.0, 20037.0, 0.0, 0.0); // "FILTERS" if (id == 25.0) return vec4(168247.0, 19.0, 0.0, 0.0); // "WIDES" if (id == 26.0) return vec4(624003.0, 3379.0, 0.0, 0.0); // "CLASSIC" if (id == 27.0) return vec4(183474.0, 5842.0, 0.0, 0.0); // "RESERVE" if (id == 28.0) return vec4(202112.0, 467988.0, 172672.0, 99840.0); // " LEFT IN THE PAC" if (id == 29.0) return vec4(875.0, 0.0, 0.0, 0.0); // "K." if (id == 30.0) return vec4(628294.0, 49160.0, 28003.0, 403091.0); // "FRESH PACK. STIL" if (id == 31.0) return vec4(183308.0, 136577.0, 27.0, 0.0); // "L SEALED." return vec4(0.0); } // length of each string, so text can be centred float strLen(float id){ if (id == 0.0) return 7.0; if (id == 1.0) return 5.0; if (id == 2.0) return 7.0; if (id == 3.0) return 5.0; if (id == 4.0) return 4.0; if (id == 5.0) return 3.0; if (id == 6.0) return 7.0; if (id == 7.0) return 5.0; if (id == 8.0) return 11.0; if (id == 9.0) return 3.0; if (id == 10.0) return 4.0; if (id == 11.0) return 6.0; if (id == 12.0) return 7.0; if (id == 13.0) return 10.0; if (id == 14.0) return 4.0; if (id == 15.0) return 6.0; if (id == 16.0) return 7.0; if (id == 17.0) return 5.0; if (id == 18.0) return 7.0; if (id == 19.0) return 5.0; if (id == 20.0) return 8.0; if (id == 21.0) return 7.0; if (id == 22.0) return 10.0; if (id == 23.0) return 6.0; if (id == 24.0) return 7.0; if (id == 25.0) return 5.0; if (id == 26.0) return 7.0; if (id == 27.0) return 7.0; return 0.0; } // Brand names are PREFIX [+ MIDDLE] + ENDING, one packed float each. float sylPre(float i){ if (i == 0.0) return 18477.0; // MAR if (i == 1.0) return 12450.0; // BEL if (i == 2.0) return 14507.0; // KEN if (i == 3.0) return 13347.0; // CAM if (i == 4.0) return 14647.0; // WIN if (i == 5.0) return 12336.0; // PAL if (i == 6.0) return 12339.0; // SAL if (i == 7.0) return 18916.0; // DOR if (i == 8.0) return 14829.0; // MON if (i == 9.0) return 3382.0; // VIC if (i == 10.0) return 25260.0; // LUX if (i == 11.0) return 24754.0; // REX if (i == 12.0) return 26098.0; // ROY if (i == 13.0) return 18467.0; // CAR if (i == 14.0) return 18484.0; // TAR if (i == 15.0) return 12342.0; // VAL if (i == 16.0) return 12595.0; // SIL if (i == 17.0) return 12775.0; // GOL if (i == 18.0) return 18926.0; // NOR if (i == 19.0) return 18472.0; // HAR if (i == 20.0) return 627971.0; // CHES if (i == 21.0) return 15012.0; // DUN if (i == 22.0) return 12333.0; // MAL if (i == 23.0) return 14380.0; // LAN if (i == 24.0) return 460354.0; // BRAN if (i == 25.0) return 595603.0; // STER if (i == 26.0) return 18915.0; // COR if (i == 27.0) return 14502.0; // FEN if (i == 28.0) return 460359.0; // GRAN if (i == 29.0) return 18923.0; // KOR if (i == 30.0) return 18608.0; // PER if (i == 31.0) return 23027.0; // SOV return 0.0; } float sylMid(float i){ if (i == 0.0) return 1.0; // A if (i == 1.0) return 15.0; // O if (i == 2.0) return 9.0; // I if (i == 3.0) return 5.0; // E if (i == 4.0) return 498.0; // RO if (i == 5.0) return 44.0; // LA if (i == 6.0) return 182.0; // VE if (i == 7.0) return 302.0; // NI return 0.0; } float sylEnd(float i){ if (i == 0.0) return 14836.0; // TON if (i == 1.0) return 25772.0; // LEY if (i == 2.0) return 670189.0; // MONT if (i == 3.0) return 19756.0; // LIS if (i == 4.0) return 15489.0; // ADO if (i == 5.0) return 20037.0; // ERS if (i == 6.0) return 12677.0; // ELL if (i == 7.0) return 184965.0; // ETTE if (i == 8.0) return 591507.0; // STAR if (i == 9.0) return 146935.0; // WOOD if (i == 10.0) return 14377.0; // IAN if (i == 11.0) return 15809.0; // ANO if (i == 12.0) return 1423.0; // OLA if (i == 13.0) return 8837.0; // ETH if (i == 14.0) return 183794.0; // ROSE if (i == 15.0) return 14644.0; // TIN if (i == 16.0) return 24050.0; // ROW if (i == 17.0) return 5577.0; // INE if (i == 18.0) return 182765.0; // MORE if (i == 19.0) return 11369.0; // ICK if (i == 20.0) return 149990.0; // FORD if (i == 21.0) return 182561.0; // AIRE if (i == 22.0) return 20929.0; // ANT if (i == 23.0) return 769.0; // AX if (i == 24.0) return 463.0; // ON if (i == 25.0) return 629.0; // US if (i == 26.0) return 15465.0; // ICO if (i == 27.0) return 504197.0; // ELLO if (i == 28.0) return 457.0; // IN if (i == 29.0) return 5711.0; // ORE if (i == 30.0) return 21061.0; // ERT if (i == 31.0) return 5569.0; // ANE return 0.0; } // Names that are not built from syllables; they fill the same three slots. #define N_HOUSE 2.0 // .xyz are the three name slots, .w packs palette + 16*layout, -1 = free vec4 nameHouse(float i){ if (i == 0.0) return vec4(313517.0, 460068.0, 0.0, 0.0); // MERIDIAN if (i == 1.0) return vec4(592675.0, 0.0, 0.0, -1.0); // CYBR return vec4(0.0, 0.0, 0.0, -1.0); } // TABLES END // ---------------------------------------------------------------- text // symbol k (0..3) of a float holding four 5-bit symbols. Every divisor // is a power of two, so this is exact. float sym4(float f, float k){ float p = k < 0.5 ? 1.0 : (k < 1.5 ? 32.0 : (k < 2.5 ? 1024.0 : 32768.0)); float q = floor(f / p); return q - 32.0 * floor(q / 32.0); } float symAt(vec4 row, float j){ float k = floor((j + 0.5) / 4.0); float f = k < 0.5 ? row.x : (k < 1.5 ? row.y : (k < 2.5 ? row.z : row.w)); return sym4(f, j - 4.0 * k); } float strChar(float id, float j){ if (j < 0.0 || j > 31.5) return 0.0; float blk = floor((j + 0.5) / 16.0); return symAt(strRow(id + blk), j - 16.0 * blk); } float slen(float v){ // symbols in a packed syllable return 1.0 + step(32.0, v) + step(1024.0, v) + step(32768.0, v); } float gS0, gS1, gS2, gL0, gL1, gL2, gNB; // brand syllables float gVar; // variant string id float gCount, gPulled, gSealed; float brandChar(float j){ if (j < 0.0) return 0.0; if (j < gL0) return sym4(gS0, j); j -= gL0; if (j < gL1) return sym4(gS1, j); j -= gL1; return j < gL2 ? sym4(gS2, j) : 0.0; } // src >= 0 : string table id // src -1 : brand name // src -2 : caption line one, "BRAND VARIANT" // src -3 : caption line two, the count or "sealed" float charOf(float src, float j){ if (src > -0.5) return strChar(src, j); if (src > -1.5) return brandChar(j); if (src > -2.5){ if (j < gNB) return brandChar(j); float v = j - gNB - 1.0; return (v < 0.0 || v > 15.5) ? 0.0 : strChar(gVar, v); } if (gSealed > 0.5) return strChar(30.0, j); float tens = floor((gCount + 0.5) / 10.0); float ones = gCount - 10.0 * tens; if (tens > 0.5){ if (j < 0.5) return 30.0 + tens; if (j < 1.5) return 30.0 + ones; return strChar(28.0, j - 2.0); } if (j < 0.5) return 30.0 + ones; return strChar(28.0, j - 1.0); } float glyphBit(float c, float col, float row){ // row 0 = top vec2 g = glyphRows(c); float bits = row < 3.5 ? sym4(g.x, row) : sym4(g.y, row - 4.0); float p = col < 0.5 ? 1.0 : (col < 1.5 ? 2.0 : (col < 2.5 ? 4.0 : (col < 3.5 ? 8.0 : 16.0))); float q = floor(bits / p); return q - 2.0 * floor(q / 2.0); } float gTJ; // index of the character the last textMask landed in // 1 where whole-texel coordinate t is ink, for n characters of src laid // out from integer origin org at integer scale sc (6x8 cells) float textMask(vec2 t, vec2 org, vec2 sc, float n, float src){ vec2 d = t - org; gTJ = -1.0; if (d.x < 0.0 || d.y < 0.0 || d.y > 7.0 * sc.y - 0.5) return 0.0; float j = floor((d.x + 0.5) / (6.0 * sc.x)); if (j > n - 0.5) return 0.0; vec2 g = floor((vec2(d.x - j * 6.0 * sc.x, d.y) + 0.5) / sc); if (g.x > 4.5) return 0.0; gTJ = j; return glyphBit(charOf(src, j), g.x, 6.0 - g.y); } // integer origin that centres n characters on c (texels) vec2 textOrg(vec2 c, vec2 sc, float n){ return floor(c - vec2(n * 6.0 * sc.x - sc.x, 7.0 * sc.y) * 0.5 + 0.5); } // ---------------------------------------------------------------- the pack #define M_FRONT 0.0 #define M_BACK 1.0 #define M_SIDER 2.0 #define M_SIDEL 3.0 #define M_TOP 4.0 #define M_BOTTOM 5.0 #define M_INSIDE 6.0 #define M_COLLAR 7.0 #define M_FILTER 8.0 #define M_CIG 9.0 #define M_CAP 10.0 #define M_FLOOR 11.0 float gW, gH, gD, gYF, gYB, gYC; // size, body top at front/back, collar top (mm) float gSty, gEmb, gFont, gWarn, gWarnF, gCork, gCollar; float gPa, gPb, gSp; // slot permutation, filter pitch vec3 gPc, gSc, gAc; // primary, secondary, accent float gHop, gSpinA; // for the floor shadow void palette(float i){ // the colour schemes of the classic packs, paraphrased if (i < 0.5){ gPc = vec3(0.78,0.07,0.09); gSc = vec3(0.96,0.95,0.92); gAc = vec3(0.10,0.08,0.08); } else if (i < 1.5){ gPc = vec3(0.80,0.62,0.26); gSc = vec3(0.97,0.96,0.92); gAc = vec3(0.36,0.25,0.10); } else if (i < 2.5){ gPc = vec3(0.05,0.52,0.34); gSc = vec3(0.95,0.96,0.94); gAc = vec3(0.92,0.52,0.12); } else if (i < 3.5){ gPc = vec3(0.14,0.33,0.74); gSc = vec3(0.92,0.94,0.97); gAc = vec3(0.70,0.72,0.78); } else if (i < 4.5){ gPc = vec3(0.07,0.07,0.08); gSc = vec3(0.84,0.68,0.33); gAc = vec3(0.90,0.75,0.38); } else if (i < 5.5){ gPc = vec3(0.10,0.13,0.36); gSc = vec3(0.96,0.95,0.92); gAc = vec3(0.80,0.12,0.12); } else if (i < 6.5){ gPc = vec3(0.93,0.77,0.30); gSc = vec3(0.44,0.27,0.12); gAc = vec3(0.97,0.95,0.88); } else if (i < 7.5){ gPc = vec3(0.44,0.24,0.55); gSc = vec3(0.86,0.86,0.89); gAc = vec3(0.95,0.80,0.40); } else if (i < 8.5){ gPc = vec3(0.90,0.44,0.10); gSc = vec3(0.97,0.93,0.84); gAc = vec3(0.35,0.18,0.08); } else { gPc = vec3(0.45,0.08,0.12); gSc = vec3(0.95,0.90,0.80); gAc = vec3(0.80,0.64,0.30); } } float lum(vec3 c){ return dot(c, vec3(0.30, 0.59, 0.11)); } // the palette colour that reads on bg, else black or white vec3 inkOn(vec3 bg){ if (abs(lum(gAc) - lum(bg)) > 0.38) return gAc; if (abs(lum(gPc) - lum(bg)) > 0.38) return gPc; return lum(bg) > 0.45 ? vec3(0.07, 0.06, 0.06) : vec3(0.97, 0.96, 0.93); } // The wrap's background artwork, in 0..1 of the front panel. Front, // back, sides and lid top all read it, so a band that crosses the front // carries on round the corner. vec3 art(vec2 nq){ vec3 c; if (gSty < 0.5){ // roof: a chevron of colour over white float roof = 0.50 + 0.17 * (1.0 - abs(nq.x - 0.5) * 2.0); c = nq.y > roof ? gPc : gSc; } else if (gSty < 1.5){ // roundel float r = length((nq - vec2(0.5, 0.60)) * vec2(gW, gH)) / gW; c = gPc; c = r < 0.40 ? gAc : c; c = r < 0.37 ? gSc : c; c = abs(r - 0.30) < 0.012 ? gPc : c; } else if (gSty < 2.5){ // bands c = gSc; c = (nq.y > 0.47 && nq.y < 0.68) ? gPc : c; c = (abs(nq.y - 0.452) < 0.009 || abs(nq.y - 0.698) < 0.009) ? gAc : c; c = nq.y > 0.93 ? gPc : c; } else if (gSty < 3.5){ // crest: dark field, double gilt rule vec2 e = min(nq, 1.0 - nq) * vec2(gW, gH); float m = min(e.x, e.y); c = ((m > 2.2 && m < 3.0) || (m > 4.0 && m < 4.6)) ? gAc : gPc; } else { // diagonal split float s = nq.y - (1.05 - 0.55 * nq.x); c = s > 0.0 ? gPc : gSc; c = abs(s) < 0.03 ? gAc : c; } return c; } float styleBrandY(){ return gSty < 0.5 ? 0.44 : (gSty < 1.5 ? 0.60 : (gSty < 2.5 ? 0.575 : (gSty < 3.5 ? 0.47 : 0.45))); } // x < 0: none. All of these sit below the lid cut at 0.78, or the emblem // rides up with the lid and leaves the pack when it opens. vec2 styleEmblem(){ // bands has no room between its band and the lid cut, so it gets none return gSty < 0.5 ? vec2(0.5, 0.585) : (gSty < 1.5 ? vec2(-1.0) : (gSty < 2.5 ? vec2(-1.0) : (gSty < 3.5 ? vec2(0.5, 0.70) : vec2(0.28, 0.70)))); } // emblems about 12 mm across; p in mm from the centre float emblem(vec2 p, float e){ if (e < 0.5){ // star float k = pow(0.5 + 0.5 * cos(5.0 * atan(p.x, p.y + 1e-4)), 3.0); return step(length(p), 2.6 + 3.6 * k); } if (e < 1.5) return step(abs(p.x) / 5.0 + abs(p.y) / 7.0, 1.0); // lozenge if (e < 2.5){ // crown float tooth = 1.0 - abs(mod(p.x + 6.0, 4.0) * 0.5 - 1.0); return step(abs(p.x), 6.0) * step(-4.0, p.y) * step(p.y, -1.0 + 5.0 * tooth); } return step(abs(p.x), 5.5) * step(p.y, 5.0) * step(-7.0 + 5.0 * p.x * p.x / 30.25, p.y); // shield } // rows of dots that read as six-point type at this resolution float finePrint(vec2 tx, float seed){ float row = tx.y - 3.0 * floor((tx.y + 0.5) / 3.0); float word = hash12(vec2(floor(tx.x / 5.0), tx.y + seed)); return (row < 0.5 && word > 0.2 && hash12(tx + seed) > 0.35) ? 1.0 : 0.0; } // Texture for material m at uv (mm). Everything is evaluated at the // centre of a whole texel, so it is point sampled exactly as a PS1 // texture page is. One text job per texel keeps the glyph table // inlined once rather than once per piece of lettering. vec3 texel(float m, vec2 uv){ float dens = m > 10.5 ? uTexel * 0.30 : uTexel; vec2 tx = floor(uv * dens); vec2 q = (tx + 0.5) / dens; vec2 WT = floor(vec2(gW, gH) * dens); vec3 tipC = gCork > 0.5 ? vec3(0.80, 0.55, 0.30) : vec3(0.90, 0.89, 0.85); vec3 fibre = vec3(0.93, 0.91, 0.84) * (0.9 + 0.1 * hash12(tx + 7.0)); vec3 col = vec3(0.8); float src = -9.0, n = 0.0; vec2 org = vec2(0.0), sc = vec2(1.0), tt = tx; vec3 ink = vec3(0.0); if (m < 1.5){ // FRONT, BACK bool back = m > 0.5; vec2 nq = q / vec2(gW, gH); col = art(nq); vec2 ep = styleEmblem(); if (!back && ep.x > 0.0 && emblem((q - ep * vec2(gW, gH)) / 1.3, gEmb) > 0.5) col = inkOn(art(ep)); float wTop = back ? 0.56 : 0.24; bool box = back || gWarnF > 0.5; float vy = box && !back ? 0.30 : 0.20; float by = back ? 0.78 : styleBrandY(); // the roundel has to hold the name INSIDE its disc, which is narrower // than the panel - fit to that, not to the panel float avail = (gSty > 0.5 && gSty < 1.5) ? WT.x * 0.70 : WT.x - 4.0; float sx = (!back && gNB * 12.0 < avail) ? 2.0 : 1.0; // 7*sy texels tall, centred on by: keep its foot clear of the variant // line by 4 texels, or a tall face collides with it float room = ((by - vy) * WT.y - 7.5) / 3.5; float sy = back ? 1.0 : min(min(max(sx, gFont + 1.0), 3.0), max(1.0, floor(room))); // A plate behind the name wherever the artwork under it is not one // flat colour: the diagonal split runs straight under it, and a name // too wide for the roundel's disc would cross the ring and lose half // its letters to the field outside. Short names keep the clean disc. vec3 plate = vec3(-1.0); if (gSty > 3.5) plate = gAc; else if (gSty > 0.5 && gSty < 1.5 && gNB * 6.0 * sx - sx > avail) plate = gSc; if (plate.x >= 0.0 && abs(nq.y - by) * WT.y < 3.5 * sy + 2.5) col = plate; vec2 lo = floor(vec2(3.0, 2.5) * dens), hi = floor(vec2(gW - 3.0, wTop * gH) * dens); if (box && tx.x > lo.x - 0.5 && tx.x < hi.x - 0.5 && tx.y > lo.y - 0.5 && tx.y < hi.y - 0.5){ // the warning: black rule, white field, two lines bool rule = tx.x < lo.x + 0.5 || tx.y < lo.y + 0.5 || tx.x > hi.x - 1.5 || tx.y > hi.y - 1.5; col = rule ? vec3(0.05) : vec3(0.97); float wsy = back ? 2.0 : 1.0; float mid = (lo.y + hi.y) * 0.5; float line = tx.y > mid ? 0.0 : 1.0; src = gWarn * 2.0 + line; n = strLen(src); sc = vec2(1.0, wsy); org = textOrg(vec2(WT.x * 0.5, mid + (line < 0.5 ? 4.5 : -4.5) * wsy), sc, n); ink = vec3(0.04); } else if (!back && abs(nq.y - vy) * gH * dens < 5.0){ src = gVar; n = strLen(gVar); sc = vec2(1.0); org = textOrg(vec2(WT.x * 0.5, vy * WT.y), sc, n); ink = inkOn(art(vec2(0.5, vy))); } else { sc = vec2(back ? 1.0 : sx, sy); src = -1.0; n = gNB; org = textOrg(vec2(WT.x * 0.5, by * WT.y), sc, n); ink = inkOn(plate.x >= 0.0 ? plate : art(vec2(0.5, by))); } } else if (m < 3.5){ // SIDES bool right = m < 2.5; float DT = floor(gD * dens); col = art(vec2(right ? 0.995 : 0.005, q.y / gH)); if (right){ // the brand, reading up the panel // The name runs the LENGTH of the panel, and the artwork behind it // changes colour part way along (a band, a roof line, a ring), so // an ink picked at the middle left the ends of the word invisible. // Give it a spine of its own and the contrast is guaranteed. if (abs(tx.y - WT.y * 0.5) < (gNB * 6.0 - 1.0) * 0.5 + 3.0) col = gPc; tt = vec2(tx.y, DT - 1.0 - tx.x); src = -1.0; n = gNB; org = textOrg(vec2(WT.y * 0.5, DT * 0.5), sc, n); ink = inkOn(gPc); } else if (q.x > 3.0 && q.x < gD - 3.0 && q.y > 6.0 && q.y < 19.0){ col = vec3(0.97); // barcode, digits under it if (q.y > 9.0) col = hash12(vec2(tx.x, 17.0)) > 0.5 || q.x < 4.2 || q.x > gD - 4.2 ? vec3(0.05) : col; else if (hash12(tx + 5.0) > 0.55 && q.y > 6.8 && q.y < 8.4) col = vec3(0.15); } else if (q.y > 24.0 && q.y < 0.62 * gH && q.x > 3.0 && q.x < gD - 3.0){ col = finePrint(tx, 3.0) > 0.5 ? inkOn(col) : col; } } else if (m < 4.5){ // LID TOP col = art(vec2(0.5, 0.995)); if (abs(q.y - (gD - 2.5)) < 0.5 / dens) col = inkOn(col); } else if (m < 5.5){ // BOTTOM col = gSc; if (q.x > 4.0 && q.x < gW - 4.0 && q.y > 4.0 && q.y < gD - 4.0 && finePrint(tx, 9.0) > 0.5) col = inkOn(gSc); } else if (m < 6.5){ // INSIDE of the card col = vec3(0.70, 0.68, 0.62) * (0.92 + 0.08 * hash12(tx)); } else if (m < 7.5){ // COLLAR col = gCollar < 0.5 ? vec3(0.93, 0.92, 0.88) : (gCollar < 1.5 ? vec3(0.72, 0.73, 0.76) * (0.85 + 0.3 * hash12(tx)) : gPc); if (q.y > gYC - 0.9) col *= 1.15; } else if (m < 8.5){ // FILTERS, seen down the collar // 7-6-7 in three rows, front row first; slot s is still in the // pack if its place in the removal order is under the count float dz = gSp * 0.866; float ri = clamp(floor(-q.y / dz + 1.5), 0.0, 2.0); float nIn = abs(ri - 1.0) < 0.5 ? 6.0 : 7.0; float ci = clamp(floor(q.x / gSp + nIn * 0.5), 0.0, nIn - 1.0); vec2 c = vec2((ci - (nIn - 1.0) * 0.5) * gSp, (1.0 - ri) * dz); float slot = ri < 0.5 ? ci : (ri < 1.5 ? 7.0 + ci : 13.0 + ci); float rank = pmod(gPa * slot + gPb, 20.0); float r = length(q - c), rc = gSp * 0.49; col = vec3(0.10, 0.08, 0.06); if (r < rc) col = rank < gCount - gPulled - 0.5 ? (r > rc - 0.9 ? tipC : fibre) : vec3(0.035, 0.03, 0.025) + 0.05 * r / rc; } else if (m < 9.5){ // CIGARETTE, v = mm down from the mouth end if (q.y < 21.0){ col = tipC; if (gCork > 0.5 && hash12(tx + 3.0) > 0.72) col *= 0.78; } else if (q.y < 22.0) col = vec3(0.86, 0.70, 0.30); else col = abs(q.y - 25.5) < 0.5 / dens ? gPc : vec3(0.95, 0.95, 0.93); } else if (m < 10.5){ // MOUTH END col = length(q) > gSp * 0.49 - 0.9 ? tipC : fibre; } else { // FLOOR: stone checker, baked spot vec2 cell = floor(q / 30.0); col = mix(vec3(0.36, 0.31, 0.27), vec3(0.10, 0.09, 0.10), mod(cell.x + cell.y, 2.0)) * (0.85 + 0.3 * hash12(tx)); vec2 g = abs(q - (cell + 0.5) * 30.0); if (max(g.x, g.y) > 15.0 - 1.0 / dens) col *= 0.55; col *= smoothstep(185.0, 40.0, length(q)) * uFloor; // blob shadow: the pack's footprint, softer and fainter with height float c = cos(gSpinA), s = sin(gSpinA); vec2 lp = vec2(c * q.x - s * q.y, s * q.x + c * q.y); vec2 e = abs(lp) - vec2(gW, gD) * 0.5; float sd = length(max(e, 0.0)) + min(max(e.x, e.y), 0.0); float soft = 3.0 + gHop * 0.25; col *= 1.0 - (1.0 - smoothstep(-soft, soft, sd - 1.0)) * mix(0.8, 0.15, clamp(gHop / 180.0, 0.0, 1.0)); } if (m < 3.5){ // the lid cut, and the tear strip on a sealed pack float z = m < 2.5 ? gD * 0.5 - q.x : q.x - gD * 0.5; float yCut = m < 0.5 ? gYF : (m < 1.5 ? gYB : gYF + (gYB - gYF) * (0.5 - z / gD)); if (abs(q.y - yCut) < 0.5 / dens) col *= 0.45; if (gSealed > 0.5 && abs(q.y - (gYF - 3.0)) < 0.9) col = vec3(0.86, 0.70, 0.30); } if (src > -8.0) col = mix(col, ink, textMask(tt, org, sc, n, src)); return col; } // ---------------------------------------------------------------- raster vec2 gP; // centre of the low-res pixel being filled vec2 gHalf; // centre of the low-res screen float gF; // focal length in low-res pixels mat3 gV; vec3 gC; // camera: world -> view rotation, position mat3 gR; vec3 gO; // pack: local -> world rotation, origin mat3 gM; vec3 gT; // pack: local -> view float gLc, gLs; // lid angle vec3 gCamL; // the camera in pack-local mm float gIW, gMat, gHit; // the winning polygon: depth key, material vec2 gUV; vec3 gCol; // its texture coordinate and vertex colour vec3 projV(vec3 v){ float iz = 1.0 / max(v.z, 1.0); vec2 s = gHalf + v.xy * (gF * iz); // the GTE hands the GPU whole pixels: every corner snaps if (uJitter > 0.0) s = floor(s / uJitter + 0.5) * uJitter; return vec3(s, iz); } vec3 pL(vec3 p){ return projV(gM * p + gT); } vec3 pW(vec3 w){ return projV(gV * (w - gC)); } float cr(vec2 a, vec2 b){ return a.x * b.y - a.y * b.x; } // The whole rasteriser. A quad is still two triangles split on A-C - that // split is where the affine kink shows, and it is the point - but the five // edge functions cover BOTH triangles (the edge function for C->A is the // negative of the one for A->C), and the attributes are interpolated ONCE, // for whichever triangle the pixel landed in. Doing it per triangle compiled // two copies of every interpolation, which was most of the shader. void quad(vec3 A, vec3 B, vec3 C, vec3 D, vec2 ta, vec2 tb, vec2 tc, vec2 td, vec3 ca, vec3 cb, vec3 cc, vec3 cd, float mat){ vec2 lo = min(min(A.xy, B.xy), min(C.xy, D.xy)); vec2 hi = max(max(A.xy, B.xy), max(C.xy, D.xy)); if (gP.x < lo.x || gP.y < lo.y || gP.x > hi.x || gP.y > hi.y) return; float eAB = cr(B.xy - A.xy, gP - A.xy); float eBC = cr(C.xy - B.xy, gP - B.xy); float eCA = cr(A.xy - C.xy, gP - C.xy); float eCD = cr(D.xy - C.xy, gP - C.xy); float eDA = cr(A.xy - D.xy, gP - D.xy); // the three edge functions of a triangle sum to twice its signed area, // so the winding test is free: a negative area is a face turned away float ar1 = eAB + eBC + eCA, ar2 = eCD + eDA - eCA; bool in1 = ar1 > 0.0 && min(eAB, min(eBC, eCA)) >= 0.0; bool in2 = ar2 > 0.0 && min(-eCA, min(eCD, eDA)) >= 0.0; if (!in1 && !in2) return; vec3 w = in1 ? vec3(eBC, eCA, eAB) : vec3(eCD, eDA, -eCA); vec3 zs = in1 ? vec3(A.z, B.z, C.z) : vec3(A.z, C.z, D.z); vec3 b = w / (in1 ? ar1 : ar2); float iw = dot(b, zs); // 1/z is linear in screen space // the ordering table: one depth for the whole polygon, as the PS1 sorted float key = mix(iw, 4.0 / (1.0 / A.z + 1.0 / B.z + 1.0 / C.z + 1.0 / D.z), uSort); if (key <= gIW) return; gIW = key; // One set of weights spans both mappings: the plain barycentric IS the // affine mapping the PS1 used, and scaling it by 1/z and renormalising is // the perspective-correct one, so the dial is a mix of the weights. vec3 wq = b * mix(zs / iw, vec3(1.0), uAffine); gUV = wq.x * ta + wq.y * (in1 ? tb : tc) + wq.z * (in1 ? tc : td); gCol = wq.x * ca + wq.y * (in1 ? cb : cc) + wq.z * (in1 ? cc : cd); gMat = mat; gHit = 1.0; } void fquad(vec3 A, vec3 B, vec3 C, vec3 D, vec2 ta, vec2 tb, vec2 tc, vec2 td, vec3 c, float mat){ quad(A, B, C, D, ta, tb, tc, td, c, c, c, c, mat); } // Per-vertex light: warm key, cool fill, ambient, and a specular the // hardware would also have had to bake into the vertex colour. Capped // at 2 because the PS1 modulated texture by vertex colour / 128. vec3 light(vec3 nLocal, vec3 pLocal){ vec3 n = gR * nLocal; vec3 v = normalize(gC - (gR * pLocal + gO)); vec3 L1 = vec3(-0.50, 0.70, 0.51), L2 = vec3(0.83, 0.17, -0.53); float sp = pow(max(dot(n, normalize(L1 + v)), 0.0), 24.0); vec3 c = vec3(0.30, 0.30, 0.36) + (vec3(0.95, 0.86, 0.72) * max(dot(n, L1), 0.0) + vec3(0.25, 0.33, 0.52) * max(dot(n, L2), 0.0)) * uLight + sp * uGloss; return min(c, vec3(2.0)); } // Box faces are wound so that cross(B-A, C-A) is the outward normal, // which projection preserves - so a negative screen area IS back-face // culling. Wrap u,v are millimetres in the CLOSED pack, which is why // the artwork runs unbroken across the lid cut until the lid moves. void drawBody(){ float x = gW * 0.5, z = gD * 0.5; vec3 lbb = pL(vec3(-x, 0.0, -z)), rbb = pL(vec3(x, 0.0, -z)); vec3 lbf = pL(vec3(-x, 0.0, z)), rbf = pL(vec3(x, 0.0, z)); vec3 ltb = pL(vec3(-x, gYB, -z)), rtb = pL(vec3(x, gYB, -z)); vec3 ltf = pL(vec3(-x, gYF, z)), rtf = pL(vec3(x, gYF, z)); vec3 k = vec3(0.0, gH * 0.4, 0.0); // A convex box can show at most three of its six faces, and WHICH three // follows from where the camera sits in the box's own frame. Picking them // with ternaries compiles three quads instead of six; when the camera is // level with a slab, neither of that pair is really visible and the // winding test culls whichever was picked. bool fz = gCamL.z > 0.0, fx = gCamL.x > 0.0, fy = gCamL.y > gYF; float yt = fz ? gYF : gYB; fquad(fz ? lbf : rbb, fz ? rbf : lbb, fz ? rtf : ltb, fz ? ltf : rtb, vec2(0.0), vec2(gW, 0.0), vec2(gW, yt), vec2(0.0, yt), light(vec3(0.0, 0.0, fz ? 1.0 : -1.0), k), fz ? M_FRONT : M_BACK); fquad(fx ? rbf : lbb, fx ? rbb : lbf, fx ? rtb : ltf, fx ? rtf : ltb, vec2(0.0), vec2(gD, 0.0), vec2(gD, fx ? gYB : gYF), vec2(0.0, fx ? gYF : gYB), light(vec3(fx ? 1.0 : -1.0, 0.0, 0.0), k), fx ? M_SIDER : M_SIDEL); fquad(fy ? ltf : lbb, fy ? rtf : rbb, fy ? rtb : rbf, fy ? ltb : lbf, fy ? vec2(0.0, gD) : vec2(0.0), fy ? vec2(gW, gD) : vec2(gW, 0.0), fy ? vec2(gW, 0.0) : vec2(gW, gD), fy ? vec2(0.0) : vec2(0.0, gD), light(vec3(0.0, fy ? 1.0 : -1.0, 0.0), k) * (fy ? 0.6 : 1.0), fy ? M_INSIDE : M_BOTTOM); } // the lid swings about the hinge at the top of the back panel vec3 lidP(vec3 p){ vec2 d = vec2(p.z + gD * 0.5, p.y - gYB); return vec3(p.x, gYB + d.x * gLs + d.y * gLc, -gD * 0.5 + d.x * gLc - d.y * gLs); } vec3 lidN(vec3 n){ return vec3(n.x, n.z * gLs + n.y * gLc, n.z * gLc - n.y * gLs); } // the inverse of lidP, so the face test can ask where the camera is in the // lid's own frame rather than the pack's vec3 lidInv(vec3 p){ vec2 e = vec2(p.z + gD * 0.5, p.y - gYB); return vec3(p.x, gYB - e.x * gLs + e.y * gLc, -gD * 0.5 + e.x * gLc + e.y * gLs); } void drawLid(){ float x = gW * 0.5, z = gD * 0.5; vec3 lbf = pL(lidP(vec3(-x, gYF, z))), rbf = pL(lidP(vec3(x, gYF, z))); vec3 lbb = pL(lidP(vec3(-x, gYB, -z))), rbb = pL(lidP(vec3(x, gYB, -z))); vec3 ltf = pL(lidP(vec3(-x, gH, z))), rtf = pL(lidP(vec3(x, gH, z))); vec3 ltb = pL(lidP(vec3(-x, gH, -z))), rtb = pL(lidP(vec3(x, gH, -z))); vec3 k = lidP(vec3(0.0, gH - 8.0, 0.0)); vec3 cl = lidInv(gCamL); // the camera in the CLOSED lid frame bool fz = cl.z > 0.0, fx = cl.x > 0.0, fy = cl.y > gH; fquad(fz ? lbf : rbb, fz ? rbf : lbb, fz ? rtf : ltb, fz ? ltf : rtb, vec2(0.0, fz ? gYF : gYB), vec2(gW, fz ? gYF : gYB), vec2(gW, gH), vec2(0.0, gH), light(lidN(vec3(0.0, 0.0, fz ? 1.0 : -1.0)), k), fz ? M_FRONT : M_BACK); fquad(fx ? rbf : lbb, fx ? rbb : lbf, fx ? rtb : ltf, fx ? rtf : ltb, vec2(0.0, fx ? gYF : gYB), vec2(gD, fx ? gYB : gYF), vec2(gD, gH), vec2(0.0, gH), light(lidN(vec3(fx ? 1.0 : -1.0, 0.0, 0.0)), k), fx ? M_SIDER : M_SIDEL); fquad(fy ? ltf : lbb, fy ? rtf : rbb, fy ? rtb : rbf, fy ? ltb : lbf, fy ? vec2(0.0, gD) : vec2(0.0), fy ? vec2(gW, gD) : vec2(gW, 0.0), fy ? vec2(gW, 0.0) : vec2(gW, gD), fy ? vec2(0.0) : vec2(0.0, gD), light(lidN(vec3(0.0, fy ? 1.0 : -1.0, 0.0)), k) * (fy ? 1.0 : 0.7), fy ? M_TOP : M_INSIDE); } // the inner frame that stands up out of the body, filters on top void drawCollar(){ float x = gW * 0.5 - 1.0, z = gD * 0.5 - 1.0, y0 = gYF - 4.0; vec3 lbb = pL(vec3(-x, y0, -z)), rbb = pL(vec3(x, y0, -z)); vec3 lbf = pL(vec3(-x, y0, z)), rbf = pL(vec3(x, y0, z)); vec3 ltb = pL(vec3(-x, gYC, -z)), rtb = pL(vec3(x, gYC, -z)); vec3 ltf = pL(vec3(-x, gYC, z)), rtf = pL(vec3(x, gYC, z)); vec3 k = vec3(0.0, gYC, 0.0); bool fz = gCamL.z > 0.0, fx = gCamL.x > 0.0; fquad(fz ? lbf : rbb, fz ? rbf : lbb, fz ? rtf : ltb, fz ? ltf : rtb, vec2(0.0, y0), vec2(2.0 * x, y0), vec2(2.0 * x, gYC), vec2(0.0, gYC), light(vec3(0.0, 0.0, fz ? 1.0 : -1.0), k), M_COLLAR); fquad(fx ? rbf : lbb, fx ? rbb : lbf, fx ? rtb : ltf, fx ? rtf : ltb, vec2(0.0, y0), vec2(2.0 * z, y0), vec2(2.0 * z, gYC), vec2(0.0, gYC), light(vec3(fx ? 1.0 : -1.0, 0.0, 0.0), k), M_COLLAR); fquad(ltf, rtf, rtb, ltb, vec2(-x, z), vec2(x, z), vec2(x, -z), vec2(-x, -z), light(vec3(0.0, 1.0, 0.0), k), M_FILTER); } // A cigarette the way a PS1 artist would build it: a hexagonal prism with // smooth vertex normals so Gouraud rounds it, two quads for the mouth end. // Only three sides can face the camera, so the walk starts at the face the // camera bears on and three side quads are compiled rather than six. void drawCig(vec2 c, float top, float rc){ vec3 tv[7]; for (int k = 0; k < 6; k++){ // the end cap needs all six float a = float(k) * (TAU / 6.0) + PI / 6.0; tv[k] = pL(vec3(c.x + cos(a) * rc, top, c.y + sin(a) * rc)); } tv[6] = tv[0]; vec2 dv = gCamL.xz - c; float k0 = ceil((atan(dv.y, dv.x) - TAU / 3.0) / (TAU / 6.0)); float len = top - gYC + 3.0; for (int j = 0; j < 3; j++){ float kk = k0 + float(j); float a0 = kk * (TAU / 6.0) + PI / 6.0, a1 = a0 + TAU / 6.0; vec3 n0 = vec3(cos(a0), 0.0, sin(a0)), n1 = vec3(cos(a1), 0.0, sin(a1)); vec2 p0 = c + n0.xz * rc, p1 = c + n1.xz * rc; vec3 b0 = pL(vec3(p0.x, gYC - 3.0, p0.y)), t0 = pL(vec3(p0.x, top, p0.y)); vec3 b1 = pL(vec3(p1.x, gYC - 3.0, p1.y)), t1 = pL(vec3(p1.x, top, p1.y)); vec3 l0 = light(n0, vec3(p0.x, top, p0.y)), l1 = light(n1, vec3(p1.x, top, p1.y)); float u = kk * rc; quad(b1, b0, t0, t1, vec2(u + rc, len), vec2(u, len), vec2(u, 0.0), vec2(u + rc, 0.0), l1, l0, l0, l1, M_CIG); } vec3 e = light(vec3(0.0, 1.0, 0.0), vec3(c.x, top, c.y)); float h = 0.866 * rc, g = 0.5 * rc; quad(tv[3], tv[2], tv[1], tv[0], vec2(-h, -g), vec2(-h, g), vec2(0.0, rc), vec2(h, g), e, e, e, e, M_CAP); quad(tv[0], tv[5], tv[4], tv[3], vec2(h, g), vec2(h, -g), vec2(0.0, -rc), vec2(-h, -g), e, e, e, e, M_CAP); } void drawFloor(){ float S = 190.0; fquad(pW(vec3(-S, 0.0, S)), pW(vec3(S, 0.0, S)), pW(vec3(S, 0.0, -S)), pW(vec3(-S, 0.0, -S)), vec2(-S, S), vec2(S, S), vec2(S, -S), vec2(-S, -S), vec3(1.0), M_FLOOR); } // ---------------------------------------------------------------- main void main(){ vec2 fragCoord = gl_FragCoord.xy; vec2 R = RENDERSIZE.xy; float lines = floor(uRes); float px = R.y / lines; vec2 L = R / px; // the low-res screen gP = floor(fragCoord / px) + 0.5; gHalf = L * 0.5; gF = uLens * lines * 0.5; float t = TIME; // ---- which pack, and how far it is through its turn on the stand float P = max(uCycle, 4.0); float kp = floor(t / P); float lt = t - kp * P; gBase = pmod(pmod(kp, 4091.0) * 37.0 + floor(uSeed) * 113.0, 4091.0); float hA = th(0.0), hB = th(1.0), hC = th(2.0), hD = th(3.0), hE = th(4.0), hF = th(5.0), hG = th(6.0); // a house brand may pin its palette and layout; -1 leaves them to the hash float house = fld(hG, 1.0, 8.0) < 0.5 ? fld(hG, 8.0, N_HOUSE) : -1.0; vec4 hn = house >= 0.0 ? nameHouse(house) : vec4(0.0, 0.0, 0.0, -1.0); float pal = hn.w >= 0.0 ? pmod(hn.w, 16.0) : fld(hA, 1.0, 10.0); palette(pal); gSty = hn.w >= 0.0 ? floor(hn.w / 16.0) : fld(hA, 10.0, 5.0); gEmb = fld(hA, 50.0, 4.0); float fmt = fld(hA, 200.0, 3.0); // king, 100s, compact gW = 55.0; gD = 22.0; gH = fmt < 0.5 ? 88.0 : (fmt < 1.5 ? 97.0 : 80.0); gYF = gH - floor(gH * 0.22 + 0.5); // the cut slopes up to the hinge gYB = gYF + 10.0; gYC = gH - 7.0; gSp = min(7.75, (gW - 2.6) / 7.0); gSealed = fld(hB, 1.0, 10.0) < 3.0 ? 1.0 : 0.0; gCount = gSealed > 0.5 ? 20.0 : 3.0 + fld(hB, 10.0, 17.0); gPulled = gSealed > 0.5 ? 0.0 : fld(hB, 170.0, 4.0); // removal order: slot s goes in turn (a*s + b) mod 20, a coprime to 20 float ai = fld(hC, 1.0, 8.0); gPa = ai < 0.5 ? 1.0 : (ai < 1.5 ? 3.0 : (ai < 2.5 ? 7.0 : (ai < 3.5 ? 9.0 : (ai < 4.5 ? 11.0 : (ai < 5.5 ? 13.0 : (ai < 6.5 ? 17.0 : 19.0)))))); float ainv = ai < 0.5 ? 1.0 : (ai < 1.5 ? 7.0 : (ai < 2.5 ? 3.0 : (ai < 3.5 ? 9.0 : (ai < 4.5 ? 11.0 : (ai < 5.5 ? 17.0 : (ai < 6.5 ? 13.0 : 19.0)))))); gPb = fld(hC, 8.0, 20.0); gWarn = fld(hC, 160.0, 4.0); gVar = 8.0 + 2.0 * pal + fld(hC, 640.0, 2.0); gS0 = sylPre(fld(hD, 1.0, 32.0)); gS1 = fld(hD, 32.0, 2.0) > 0.5 ? sylMid(fld(hD, 64.0, 8.0)) : 0.0; gS2 = sylEnd(fld(hE, 1.0, 32.0)); // one pack in eight carries a house brand instead of a built name; it // fills the same three slots, so everything downstream is unchanged if (house >= 0.0){ gS0 = hn.x; gS1 = hn.y; gS2 = hn.z; } // a slot is empty only when it is zero - no syllable packs to zero, since // every letter is at least one gL0 = gS0 > 0.5 ? slen(gS0) : 0.0; gL1 = gS1 > 0.5 ? slen(gS1) : 0.0; gL2 = gS2 > 0.5 ? slen(gS2) : 0.0; gNB = gL0 + gL1 + gL2; gCork = fld(hE, 32.0, 3.0) < 2.0 ? 1.0 : 0.0; gCollar = fld(hE, 96.0, 3.0); gFont = fld(hE, 288.0, 3.0); gWarnF = fld(hF, 64.0, 5.0) < 4.0 ? 1.0 : 0.0; // ---- drop in, bounce, open, show, close, lift out. All closed form. float drop = 260.0 * exp(-3.2 * lt) * abs(cos(PI * 2.3 * lt)); float lift = smoothstep(P - 0.5, P, lt); gHop = drop + 260.0 * lift * lift; float open = gSealed > 0.5 ? 0.0 : smoothstep(1.5, 2.4, lt) * (1.0 - smoothstep(P - 1.4, P - 0.7, lt)) * uLid; float ang = open * (100.0 + 8.0 * fld(hF, 640.0, 3.0)) * (PI / 180.0); gLc = cos(ang); gLs = sin(ang); float rise = smoothstep(0.55, 1.0, open) * uCigs; gSpinA = uSpin * t; float tum = (fld(hF, 320.0, 2.0) * 2.0 - 1.0) * 0.0022 * gHop; // tumbles in the air, upright on landing float cs = cos(gSpinA), sn = sin(gSpinA), ct = cos(tum), st = sin(tum); gR = mat3(cs, 0.0, -sn, 0.0, 1.0, 0.0, sn, 0.0, cs) * mat3(ct, st, 0.0, -st, ct, 0.0, 0.0, 0.0, 1.0); gO = vec3(0.0, gH * 0.5 + gHop, 0.0) - gR * vec3(0.0, gH * 0.5, 0.0); float az = 0.14 * sin(0.21 * t) * uSway; float el = uElev + 0.05 * sin(0.33 * t + 1.0) * uSway; vec3 tgt = vec3(0.0, 46.0, 0.0); gC = tgt + 300.0 * vec3(sin(az) * cos(el), sin(el), cos(az) * cos(el)); vec3 f = normalize(tgt - gC); vec3 r = normalize(cross(f, vec3(0.0, 1.0, 0.0))); vec3 u = cross(r, f); gV = mat3(r.x, u.x, f.x, r.y, u.y, f.y, r.z, u.z, f.z); gM = gV * gR; gT = gV * (gO - gC); gCamL = (gC - gO) * gR; // gR is a rotation, so this is its transpose // ---- rasterise gIW = 0.0; gHit = 0.0; gMat = 0.0; gUV = vec2(0.0); gCol = vec3(0.0); if (uFloor > 0.0) drawFloor(); drawBody(); drawCollar(); drawLid(); // the next ones due out of the pack stand proud of it: removal turns // count-1, count-2, count-3, found by inverting the permutation for (int i = 0; i < 3; i++){ float fi = float(i); if (fi < gPulled - 0.5){ float slot = pmod(ainv * (gCount - 1.0 - fi + 20.0 - gPb), 20.0); float ri = slot < 6.5 ? 0.0 : (slot < 12.5 ? 1.0 : 2.0); float ci = slot - (ri < 0.5 ? 0.0 : (ri < 1.5 ? 7.0 : 13.0)); float nIn = abs(ri - 1.0) < 0.5 ? 6.0 : 7.0; vec2 c = vec2((ci - (nIn - 1.0) * 0.5) * gSp, (1.0 - ri) * gSp * 0.866); float up = rise * (9.0 + 7.0 * fld(hF, fi < 0.5 ? 1.0 : (fi < 1.5 ? 4.0 : 16.0), 4.0)); if (up > 0.0) drawCig(c, gYC - 1.0 + up, gSp * 0.49 * 1.04); } } // ---- shade: texture MODULATED by vertex colour, then depth cue vec2 sq = gP / L; vec3 fogC = vec3(0.012, 0.010, 0.018); vec3 col = mix(fogC, vec3(0.075, 0.065, 0.11), smoothstep(0.25, 1.0, sq.y)); if (gHit > 0.5){ col = texel(gMat, gUV) * gCol; col = mix(col, fogC, uFog * smoothstep(260.0, 560.0, 1.0 / gIW)); } col *= uExpo; // ---- the examine caption, in a menu window of the period if (uHud > 0.5){ vec2 pi = floor(gP); vec2 b0 = vec2(8.0, 6.0), b1 = vec2(floor(L.x) - 8.0, 36.0); vec2 e = min(pi - b0, b1 - 1.0 - pi); if (min(e.x, e.y) >= 0.0 && e.x + e.y >= 2.0){ float bd = min(e.x, e.y); vec3 box = mix(vec3(0.0, 0.01, 0.20), vec3(0.03, 0.10, 0.52), (pi.y - b0.y) / (b1.y - b0.y)) * (1.05 - 0.3 * (pi.x - b0.x) / (b1.x - b0.x)); col = bd < 0.5 ? vec3(0.82) : (bd < 1.5 ? vec3(0.50) : box); // one character every 45 ms once the pack has landed float vis = floor((lt - 0.9) * 22.0); // line one occupies rows 23-29, line two rows 12-18: split in // the gap between them, never inside a line's own glyph rows bool one = pi.y > b1.y - 15.5; vec2 o = vec2(b0.x + 6.0, b1.y - (one ? 13.0 : 24.0)); float src = one ? -2.0 : -3.0, off = one ? 0.0 : 40.0; // gTJ is set by the call, so read it in a separate statement - // operand evaluation order within an expression is undefined float sh = textMask(pi + vec2(-1.0, 1.0), o, vec2(1.0), 40.0, src); sh *= step(gTJ + off, vis - 0.5); float tm = textMask(pi, o, vec2(1.0), 40.0, src); tm *= step(gTJ + off, vis - 0.5); col = mix(col, vec3(0.04), sh); col = mix(col, one ? vec3(0.96, 0.96, 0.92) : vec3(0.76, 0.80, 0.90), tm); } } // ---- 15-bit colour through the PS1's own ordered dither vec2 pi = floor(gP); vec2 a1 = pi - 2.0 * floor(pi * 0.5); vec2 a2 = floor(pi * 0.5) - 2.0 * floor(pi * 0.25); float bay = 4.0 * (2.0 * a1.x + 3.0 * a1.y - 4.0 * a1.x * a1.y) + (2.0 * a2.x + 3.0 * a2.y - 4.0 * a2.x * a2.y); float d8 = (floor(bay * 0.5) - 4.0) * uDither; float lv = floor(pow(2.0, floor(uBits)) + 0.5); vec3 c8 = clamp(col * 255.0 + d8, 0.0, 255.0); col = min(floor(c8 / (256.0 / lv)), lv - 1.0) / (lv - 1.0); gl_FragColor = vec4(col, 1.0); }
A procedurally generated pack of cigarettes turning on a PlayStation-1 'examine item' screen. It is not a raymarcher wearing a pixel filter: it is a small software rasteriser, and every PS1 artefact comes from doing what the PS1 did. Vertices are projected onto a 240-line screen and ROUNDED to whole pixels, as the GTE handed the GPU integer coordinates, so every corner jumps pixel to pixel as the pack turns and the edges wobble. Each quad is two triangles filled from those snapped corners, with barycentrics taken in screen space and used as they are - no divide by depth - so the textures swim and kink along each quad's diagonal exactly as affine mapping did (the perspective-correct answer is computed too, and a dial blends to it). Texels are point sampled, light is per vertex and MODULATES the texture up to twice as bright, colour is 15-bit, and the ordered dither is the PS1's own 4x4 matrix, floor(bayer/2) - 4 in 8-bit steps. Back-face culling is free: the faces are wound so cross(B-A, C-A) is the outward normal, projection keeps the winding, and a negative screen area is a face turned away. An optional ordering-table mode sorts by one depth per polygon, as the hardware did, instead of per pixel. Each pack comes from one integer: size (king, 100s, compact), a palette, one of five wrap layouts, an emblem, a brand name built from syllables - or, one pack in eight, a house brand the syllables cannot reach - a variant word, a warning, sealed or opened, how many are left, which slots are empty and which cigarettes stand proud. The filters seen down the open lid ARE the count: the twenty slots are taken in the order (a*s + b) mod 20 with a coprime to 20, a slot is still full if its turn is under the count, and the ones standing proud are the next three due, found by inverting the permutation - so the caption, the holes and the raised cigarettes cannot disagree. All of those decisions are exact integer arithmetic carried in floats, every intermediate whole and under 2^24, and all text packs four 5-bit symbols to a float so every divide is by a power of two and exact. The wrap artwork is drawn in millimetres of the CLOSED pack, so the design runs unbroken across the lid cut until the lid swings. Controls: resolution, vertex snap, affine amount, dither, colour depth, texel density, spin, focal length, elevation, camera sway, seconds per pack, seed, lid, pulled cigarettes, key light, gloss, fog, floor, caption, ordering-table sort and exposure.
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).