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