Nimbin[12]?Games / Entropie / entropie/assets/shaders/figure.frag

Entropie git · main

Entropie festival game; first steps

sdl c game entropie cmake · first commit 2026-10-10 · last commit 2026-11-02 (19 hours ago) · synced 10 hours ago

C 88.5% Markdown 9.1% GLSL 2.3%
git clone https://git.christianimmanuel.de/games/Entropie.gitwget https://git.christianimmanuel.de/games/Entropie/archive/Entropie.tar.gz
entropie/assets/shaders/figure.frag 29.1 KB · 567 lines raw
// 2D painted figure, rotoscope style.
//
// The skeleton (game/body.c) is wrapped in a smooth body: tapered capsules
// blended into each other, traced along the view ray from the card. The
// result is shaded with the same flat paint tones as the world and writes
// its real depth, so arms pass in front of the body and items sit in hands.

in vec3 CardWorld;

layout (location = 0) out vec4 OutColor;
layout (location = 1) out vec4 OutNormalDepth;

// Joints in figure space (x = right, y = up, z = forward), see render/figure.h
const int J_PELVIS = 0, J_CHEST = 1, J_NECK = 2, J_HEAD = 3;
const int J_HIP_L = 4, J_KNEE_L = 5, J_ANKLE_L = 6, J_TOE_L = 7;
const int J_HIP_R = 8, J_KNEE_R = 9, J_ANKLE_R = 10, J_TOE_R = 11;
const int J_SHOULDER_L = 12, J_ELBOW_L = 13, J_WRIST_L = 14, J_HAND_L = 15;
const int J_SHOULDER_R = 16, J_ELBOW_R = 17, J_WRIST_R = 18, J_HAND_R = 19;
uniform vec3 joints[20];

uniform bool  shadowPass;  // rays along the light instead of from the camera
uniform vec3  figurePos;
uniform vec3  facing;      // figure forward, horizontal unit vector
uniform int   hairStyle;   // 0 = short, 1 = long
uniform bool  longSleeves;
uniform int   castLimb;     // a plaster cast: 0 none, 1 left forearm, 2 right forearm, 3 left shin, 4 right shin
uniform vec3  headForward;  // face direction (figure space)
uniform vec3  headUp;
uniform vec3  chestForward; // front of the torso
uniform vec3  waistBend;    // the waist off the straight pelvis-chest line (the back curves)
uniform vec3  face;         // mouth open, eyelids closed, brow (-1 shy .. 1 raised)
uniform vec2  expression;   // smile (-1 .. 1), gaze (-1 down .. 1 up)
uniform vec3  skinColor;
uniform vec3  hairColor;
uniform vec3  shirtColor;
uniform vec3  jacketColor;  // (top == 3: a jacket over it)
uniform bool  jacketTied;   // the jacket off, tied round the waist by its sleeves
uniform float belly;        // 0 .. 1: a belly out in front (Kurti's)
uniform vec3  pantsColor;
uniform vec3  shoeColor;
uniform vec3  stream[4];    // pee or sick: an arc from the body to the ground (figure space)
uniform int   streamKind;   // 0 none, 1 pee, 2 sick, 3 water
uniform float streamWidth;
uniform int   garment;      // 0 trousers, 1 shorts, 2 skirt, 3 dress
uniform float soaked;       // 0..1 wet from the rain: clothes and hair darker
uniform bool  hood;         // the jacket's hood up over the head (rain)
uniform int   hat;          // 0 none, 1 a cap (a brim in front), 2 a beanie (knitted, a turned-up cuff)
uniform vec3  hatColor;
uniform float undress;      // to pee: squatting trousers down / skirt up (0..1); standing fly open / skirt lifted in front (-1..0)
uniform vec3  cigA, cigTip; // a cigarette (figure space): the end at the hand / lips, the burning end
uniform float cigGlow;
uniform vec3  uriA, uriB;   // a Urinella held to the body (figure space)
uniform bool  uri;      // -1 none, 0 not lit .. 1 a drag
uniform int   species;      // 0 a person, 1 the dog (the joints mean her body, see dog.c)
uniform vec3  tailA, tailB; // the dog's tail
uniform int   top;          // 0 a shirt, 1 bare-chested, 2 just a top (shirt off in the sun), 3 a jacket (cold night)

uniform mat4  viewProj;
uniform mat4  view;
uniform mat4  lightSpaceMatrix;
uniform vec3  viewPos;
uniform vec3  lightDirection;
uniform vec3  lightColor;
uniform vec3  shadowColor;
uniform vec3  skyTop;
uniform vec3  skyHorizon;
uniform float shadowStrength;
uniform float boilTime;
uniform sampler2D shadowMap;

const int M_SKIN = 0, M_HAIR = 1, M_SHIRT = 2, M_PANTS = 3, M_SHOES = 4, M_PEE = 5, M_SICK = 6, M_WATER = 7, M_CIG = 8, M_JACKET = 9, M_URINELLA = 10;
const int M_FUR = 11, M_FUR_DARK = 12, M_NOSE = 13, M_HAT = 14, M_CAST = 15;

const float FIGURE_LIGHT_WRAP = 0.6;  // soft, flat faces instead of a hard light split
const float SELF_SHADOW_SKIP  = 0.10; // meters

// ---------- Body parts ----------

const int MAX_PARTS = 42; // (a person with everything: a skirt, a jacket tied round, a cigarette, a Urinella, a stream)
vec3  partA[MAX_PARTS], partB[MAX_PARTS];
float partR1[MAX_PARTS], partR2[MAX_PARTS];
float partBlend[MAX_PARTS];   // smooth-union radius with the rest of the body
float partDepth[MAX_PARTS];   // < 1 flattens the part front to back (torso, pelvis)
int   partMat[MAX_PARTS];
int   partCount = 0;
vec3  headPos, headR, headU, headF;  // head frame (figure space)
vec3  spineDir;

// Head-local offset -> figure space, and back
vec3 onHead(vec3 o) { return headPos + headR * o.x + headU * o.y + headF * o.z; }
vec3 toHead(vec3 p) { vec3 d = p - headPos; return vec3(dot(d, headR), dot(d, headU), dot(d, headF)); }
int   candidates = 0;         // bit mask of parts near the current ray
bool  hairCandidate = false;

void addPart(vec3 a, vec3 b, float r1, float r2, float blend, float depth, int mat) {
    if (partCount >= MAX_PARTS) return;
    partA[partCount] = a;     partB[partCount] = b;
    partR1[partCount] = r1;   partR2[partCount] = r2;
    partBlend[partCount] = blend; partDepth[partCount] = depth;
    partMat[partCount] = mat;
    partCount++;
}

// The dog: a long low body, short legs, a long head with floppy ears, a thin tail.
// Joints: pelvis = her hindquarters, chest = her chest, neck, head; the hind legs in
// the leg slots (hip, knee, ankle, toe), the front legs in the arm slots.
void buildDog() {
    vec3 hind = joints[J_PELVIS], fore = joints[J_CHEST], neck = joints[J_NECK];
    addPart(hind, fore, 0.075, 0.085, 0.04, 1.0, M_FUR);                               // the long body
    addPart(fore + vec3(0.0, 0.01, 0.03), neck, 0.060, 0.048, 0.04, 1.0, M_FUR);        // neck
    addPart(onHead(vec3(0.0, 0.005, -0.01)), onHead(vec3(0.0, 0.0, 0.02)), 0.058, 0.055, 0.03, 1.0, M_FUR); // skull
    addPart(onHead(vec3(0.0, -0.005, 0.04)), onHead(vec3(0.0, -0.022, 0.125)), 0.040, 0.024, 0.025, 1.0, M_FUR); // snout
    addPart(onHead(vec3(0.0, -0.016, 0.142)), onHead(vec3(0.0, -0.016, 0.142)), 0.015, 0.015, 0.006, 1.0, M_NOSE);
    for (int s = 0; s < 2; s++) {
        float x = s == 0 ? -1.0 : 1.0;
        addPart(onHead(vec3(x * 0.050, 0.025, -0.012)), onHead(vec3(x * 0.066, -0.070, -0.004)), 0.018, 0.027, 0.012, 0.6, M_FUR_DARK); // floppy ears
        int o = s * 4;
        // hind leg (thigh, shank, paw) and fore leg
        addPart(joints[J_HIP_L + o], joints[J_KNEE_L + o], 0.040, 0.026, 0.03, 1.0, M_FUR);
        addPart(joints[J_KNEE_L + o], joints[J_ANKLE_L + o], 0.024, 0.021, 0.015, 1.0, M_FUR);
        addPart(joints[J_ANKLE_L + o], joints[J_TOE_L + o], 0.022, 0.020, 0.012, 1.0, M_FUR_DARK);
        addPart(joints[J_SHOULDER_L + o], joints[J_ELBOW_L + o], 0.038, 0.026, 0.03, 1.0, M_FUR);
        addPart(joints[J_ELBOW_L + o], joints[J_WRIST_L + o], 0.024, 0.021, 0.015, 1.0, M_FUR);
        addPart(joints[J_WRIST_L + o], joints[J_HAND_L + o], 0.022, 0.020, 0.012, 1.0, M_FUR_DARK);
    }
    addPart(tailA, tailB, 0.020, 0.007, 0.02, 1.0, M_FUR);
    if (streamKind > 0) // (she pees too)
        for (int k = 0; k < 3; k++) addPart(stream[k], stream[k + 1], streamWidth, streamWidth * 0.8, 0.002, 1.0, M_PEE);
}

void buildBody() {
    if (species == 1) { buildDog(); return; }
    vec3 pelvis = joints[J_PELVIS], chest = joints[J_CHEST], neck = joints[J_NECK];
    vec3 waist = mix(pelvis, chest, 0.45) + waistBend;
    bool jacket = top == 3;
    int forearm = jacket ? M_JACKET : longSleeves && top == 0 ? M_SHIRT : M_SKIN;
    int sleeve  = jacket ? M_JACKET : top == 0 ? M_SHIRT : M_SKIN;
    float puff  = jacket ? 0.008 : 0.0; // (a jacket is a bit bulkier)

    // Torso: belly into chest, shoulders across (the shirt off: skin, or just a top; a jacket over it)
    addPart(pelvis + spineDir * (jacket ? 0.0 : 0.05), waist, 0.130 + puff, 0.135 + puff, 0.04, 0.70, sleeve);
    addPart(waist, chest + spineDir * 0.03 - chestForward * 0.005, 0.135 + puff, 0.160 + puff, 0.04, 0.66, jacket ? M_JACKET : top == 1 ? M_SKIN : M_SHIRT);
    addPart(joints[J_SHOULDER_L], joints[J_SHOULDER_R], 0.050 + puff, 0.050 + puff, 0.05, 0.75, sleeve);
    if (belly > 0.0) { // a heavy build: one soft rounded front from the chest down over the belt (not a ball stuck on)
        int front = jacket ? M_JACKET : top == 1 ? M_SKIN : M_SHIRT;
        vec3 across = normalize(joints[J_SHOULDER_R] - joints[J_SHOULDER_L]);
        vec3 b = mix(pelvis, chest, 0.32) + chestForward * (0.055 * belly);              // the belly, low and broad
        addPart(b - spineDir * 0.09, b + spineDir * 0.10, 0.14 + 0.045 * belly, 0.13 + 0.04 * belly, 0.11, 0.9, front);
        vec3 c = chest - spineDir * 0.07 + chestForward * (0.035 * belly);               // the chest, soft, on into the belly
        for (int s = -1; s <= 1; s += 2)
            addPart(c + across * (float(s) * 0.07) - spineDir * 0.02, c + across * (float(s) * 0.075) + spineDir * 0.03,
                    0.075 + 0.02 * belly, 0.07 + 0.015 * belly, 0.10, 0.85, front);
        addPart(pelvis + spineDir * 0.12 - across * 0.12, pelvis + spineDir * 0.12 + across * 0.12, // the sides over the hips
                0.07 + 0.025 * belly, 0.07 + 0.025 * belly, 0.09, 0.8, front);
    }

    // A jacket tied round the waist: a band over the hips, the sleeves knotted in front, the rest hanging down behind
    if (jacketTied) {
        vec3 belt = pelvis + spineDir * 0.07;
        addPart(belt - spineDir * 0.025, belt + spineDir * 0.025, 0.142, 0.142, 0.02, 0.72, M_JACKET);
        vec3 knot = belt + chestForward * (garment >= 2 ? 0.168 : 0.125); // (over a skirt's top)
        addPart(knot, knot, 0.026, 0.026, 0.01, 1.0, M_JACKET);
        vec3 across = normalize(joints[J_HIP_R] - joints[J_HIP_L]);
        for (int s = 0; s < 2; s++) { // (the sleeves' ends, pushed out in front by a thigh coming up)
            float len = s == 0 ? 0.17 : 0.14;
            vec3 end = knot - spineDir * len + chestForward * 0.02 + across * (s == 0 ? -0.03 : 0.03);
            vec3 hip = joints[J_HIP_L + s * 4], knee = joints[J_KNEE_L + s * 4], leg = knee - hip;
            float t = clamp(dot(end - hip, leg) / max(dot(leg, leg), 1e-4), 0.0, 1.0);
            float ahead = dot(mix(hip, knee, t) - end, chestForward) + mix(0.085, 0.058, t) + 0.02;
            if (ahead > 0.0) end = knot + normalize(end + chestForward * ahead - knot) * len;
            addPart(knot, end, 0.022, 0.018, 0.01, 1.0, M_JACKET);
        }
        // the back hangs in two halves, each pushed out behind by its thigh going back (no leg through it)
        vec3 back = belt - chestForward * 0.085;
        for (int s = 0; s < 2; s++) {
            vec3 top = back + across * (s == 0 ? -0.055 : 0.055);
            vec3 hang = top - spineDir * 0.27 - chestForward * 0.02;
            vec3 hip = joints[J_HIP_L + s * 4], knee = joints[J_KNEE_L + s * 4], leg = knee - hip;
            float t = clamp(dot(hang - hip, leg) / max(dot(leg, leg), 1e-4), 0.0, 1.0);
            float behind = dot(mix(hip, knee, t) - hang, -chestForward) + mix(0.085, 0.058, t) + 0.03;
            if (behind > 0.0) hang = top + normalize(hang - chestForward * behind - top) * 0.27;
            addPart(top, hang, 0.072, 0.078, 0.015, 0.25, M_JACKET);
        }
    }

    // What's on the legs: trousers, shorts, a skirt or a dress (squatting to pee:
    // trousers pulled down to the knees, a skirt or dress gathered up)
    bool down = undress > 0.5, lifted = undress < -0.5;
    int lower = garment == 3 ? M_SHIRT : M_PANTS;
    int hips  = (garment == 0 && down) ? M_SKIN : lower;
    // (under a skirt or dress the thighs wear it too: where a leg presses out, it's cloth, not leg)
    int thigh = garment >= 2 ? (down || lifted ? M_SKIN : lower) : (garment == 0 && !down) || garment == 1 ? M_PANTS : M_SKIN;
    int shin  = garment == 0 ? M_PANTS : M_SKIN;

    // Pelvis spans the hips, so the thighs grow out of it
    addPart(joints[J_HIP_L] + spineDir * 0.04, joints[J_HIP_R] + spineDir * 0.04,
            0.100, 0.100, 0.05, 0.78, hips);

    // A skirt or dress: a cone from the waist down to the knees (wider as they part)
    if (garment >= 2) {
        vec3 knees = 0.5 * (joints[J_KNEE_L] + joints[J_KNEE_R]);
        vec3 waistline = pelvis + spineDir * 0.07;
        float lift = max(-undress, 0.0); // (standing: gathered up in front, held at the hips)
        vec3 hem = mix(knees + vec3(0.0, -0.06, 0.0), waistline, 0.6 * max(undress, 0.0) + 0.72 * lift)
                 + chestForward * 0.09 * lift;
        float apart = 0.5 * length(joints[J_KNEE_L] - joints[J_KNEE_R]);
        float wide = (apart + 0.08) * (1.0 - 0.3 * lift);
        // sitting (the thighs out forward): laid over each thigh (the two melt into one
        // lap), the hem falling a little in front of the knees - not a sack round them
        float sit = clamp((0.8 - dot(normalize(knees - pelvis), -spineDir)) / 0.6, 0.0, 1.0);
        if (sit < 0.5 || lift > 0.0 || undress > 0.0) {
            addPart(waistline, hem, 0.165, max(wide, 0.165), 0.03, 0.8, lower); // (over the hips and thighs, wider as the knees part)
        } else {
            for (int s = 0; s < 2; s++) {
                vec3 hip = joints[J_HIP_L + s * 4] + spineDir * 0.04, knee = joints[J_KNEE_L + s * 4];
                addPart(hip, knee + normalize(knee - hip) * 0.02, 0.112, 0.098, 0.06, 1.0, lower);
            }
            vec3 front = knees + chestForward * 0.04;
            addPart(front, front + vec3(0.0, -0.09, 0.0), apart + 0.03, apart + 0.045, 0.04, 0.3, lower);
        }
    }

    // Legs and feet
    for (int s = 0; s < 2; s++) {
        int o = s * 4;
        vec3 knee = joints[J_KNEE_L + o], ankle = joints[J_ANKLE_L + o], toe = joints[J_TOE_L + o];
        vec3 heel = ankle - normalize(toe - ankle) * 0.03 + normalize(ankle - knee) * 0.02;
        addPart(joints[J_HIP_L + o], knee, 0.085, 0.058, 0.05, 1.0, thigh);
        if (castLimb == 3 + s) addPart(knee - normalize(ankle - knee) * 0.01, ankle + normalize(ankle - knee) * 0.03, 0.066, 0.056, 0.02, 1.0, M_CAST); // (a cast on the shin)
        else addPart(knee, ankle, 0.055, 0.040, 0.02, 1.0, shin);
        addPart(ankle, toe, 0.045, 0.034, 0.02, 1.0, M_SHOES);
        addPart(heel, heel, 0.045, 0.045, 0.02, 1.0, M_SHOES);
    }

    // Arms and hands
    for (int s = 0; s < 2; s++) {
        int o = s * 4;
        addPart(joints[J_SHOULDER_L + o], joints[J_ELBOW_L + o], 0.055 + puff, 0.045 + puff, 0.035, 1.0, sleeve);
        if (castLimb == 1 + s) addPart(joints[J_ELBOW_L + o], joints[J_WRIST_L + o] + normalize(joints[J_HAND_L + o] - joints[J_WRIST_L + o]) * 0.03, 0.054, 0.047, 0.02, 1.0, M_CAST); // (a cast on the forearm)
        else addPart(joints[J_ELBOW_L + o], joints[J_WRIST_L + o], 0.044 + puff, 0.034 + puff * 0.5, 0.02, 1.0, forearm);
        addPart(joints[J_WRIST_L + o], joints[J_HAND_L + o], 0.033, 0.036, 0.015, 1.0, M_SKIN);
    }

    // Neck and head: cranium, jaw, nose, ears
    addPart(chest + normalize(neck - chest) * 0.06, neck, 0.050, 0.046, 0.04, 1.0, M_SKIN);
    addPart(onHead(vec3(0.0, 0.015, -0.010)), onHead(vec3(0.0, 0.015, -0.010)), 0.095, 0.095, 0.04, 1.0, M_SKIN);
    addPart(onHead(vec3(0.0, -0.050, 0.030)), onHead(vec3(0.0, -0.005, 0.045)), 0.058, 0.070, 0.04, 1.0, M_SKIN);
    addPart(onHead(vec3(0.0, 0.000, 0.090)), onHead(vec3(0.0, -0.022, 0.110)), 0.010, 0.016, 0.012, 1.0, M_SKIN);
    addPart(onHead(vec3(-0.090, -0.005, -0.005)), onHead(vec3(-0.090, -0.020, -0.005)), 0.020, 0.018, 0.01, 1.0, M_SKIN);
    addPart(onHead(vec3( 0.090, -0.005, -0.005)), onHead(vec3( 0.090, -0.020, -0.005)), 0.020, 0.018, 0.01, 1.0, M_SKIN);

    // A cigarette
    if (cigGlow >= 0.0) addPart(cigA, cigTip, 0.0045, 0.0042, 0.001, 1.0, M_CIG);
    // A Urinella: a little funnel from the body to the spout
    if (uri) addPart(uriA, uriB, 0.032, 0.009, 0.002, 1.0, M_URINELLA);

    // A stream down to the ground, thinning out (pee) or in clumps (sick)
    if (streamKind > 0) {
        int m = streamKind == 1 ? M_PEE : streamKind == 3 ? M_WATER : M_SICK;
        for (int k = 0; k < 3; k++) {
            float w0 = streamWidth * (1.0 - 0.15 * float(k)), w1 = streamWidth * (0.85 - 0.15 * float(k));
            addPart(stream[k], stream[k + 1], w0, w1, 0.002, 1.0, m);
        }
    }
}

// ---------- Distance field ----------

float smin(float a, float b, float k) {
    float h = max(k - abs(a - b), 0.0) / k;
    return min(a, b) - h * h * k * 0.25;
}

// Stretches space along the torso's front so flattened parts stay flat however the body lies.
vec3 unflatten(vec3 p, float s) { return p + chestForward * (dot(p, chestForward) * (1.0 / s - 1.0)); }

float partDistance(int i, vec3 p) {
    float s = partDepth[i];
    vec3 q = p, a = partA[i], b = partB[i];
    if (s < 1.0) { q = unflatten(p, s); a = unflatten(a, s); b = unflatten(b, s); }
    vec3 ba = b - a;
    float h = clamp(dot(q - a, ba) / max(dot(ba, ba), 1e-8), 0.0, 1.0);
    return (length(q - a - ba * h) - mix(partR1[i], partR2[i], h)) * s;
}

float hairDistance(vec3 world_p) {
    vec3 p = toHead(world_p), h = vec3(0.0); // head-local
    float cap      = length(p - (h + vec3(0.0, 0.022, -0.012))) - 0.104;
    float hairline = dot(p - (h + vec3(0.0, 0.040, 0.070)), normalize(vec3(0.0, 1.0, -0.9)));
    float d = max(cap, -hairline);
    if (hairStyle == 0) {
        float sides = min((h.y - 0.015) - p.y, p.z - (h.z - 0.030)); // keep ears free
        d = max(d, sides);
    } else {
        vec3 a = h + vec3(0.0, 0.0, -0.055), b = h + vec3(0.0, -0.23, -0.07);
        vec3 ba = b - a;
        float t = clamp(dot(p - a, ba) / dot(ba, ba), 0.0, 1.0);
        float back = length(p - a - ba * t) - mix(0.095, 0.075, t);
        back = max(back, p.z - (h.z - 0.005));
        d = smin(d, back, 0.03);
    }
    return d;
}

// A hat on the head (head-local): a cap's crown and its brim out in front, or a beanie down to the ears.
float hatDistance(vec3 world_p) {
    vec3 p = toHead(world_p);
    if (hat == 2) {
        float crown = length(p - vec3(0.0, 0.028, -0.014)) - 0.112;
        float rim = dot(p - vec3(0.0, -0.012, 0.0), normalize(vec3(0.0, -1.0, 0.25))); // (the cuff's lower edge, a bit lower behind)
        float top = length(p - vec3(0.0, 0.128, -0.02)) - 0.018;                       // (a little knot on top)
        return min(max(crown, rim), top);
    }
    float crown = max(length(p - vec3(0.0, 0.026, -0.012)) - 0.108, dot(p - vec3(0.0, 0.020, 0.0), normalize(vec3(0.0, -1.0, 0.15))));
    vec3 b = p - vec3(0.0, 0.026, 0.115);                                                  // the brim: a thin slab out over the brow
    b.y += 0.12 * b.z;                                                                     // (tipped down a little)
    vec3 q = abs(b) - vec3(0.075, 0.004, 0.055);
    float brim = length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0) - 0.004;
    return min(crown, brim);
}

// The hood up: a shell round the head, open for the face, down the back of the neck.
float hoodDistance(vec3 world_p) {
    vec3 p = toHead(world_p);
    float shell = length(p - vec3(0.0, 0.020, -0.018)) - 0.126;
    float open  = dot(p - vec3(0.0, 0.0, 0.050), normalize(vec3(0.0, -0.30, 1.0))); // (the face out of it)
    float d = max(shell, open);
    vec3 a = vec3(0.0, -0.030, -0.050), b = vec3(0.0, -0.160, -0.070), ba = b - a;
    float t = clamp(dot(p - a, ba) / dot(ba, ba), 0.0, 1.0);
    float back = max(length(p - a - ba * t) - mix(0.090, 0.075, t), p.z - 0.005);
    return smin(d, back, 0.03);
}

float body(vec3 p, out int mat) {
    float d = 1e9, nearest = 1e9;
    mat = M_SKIN;
    for (int i = 0; i < partCount; i++) {
        if ((candidates & (1 << i)) == 0) continue;
        float di = partDistance(i, p);
        d = smin(d, di, partBlend[i]);
        if (di < nearest) { nearest = di; mat = partMat[i]; }
    }
    if (hairCandidate) {
        float dh = hood ? hoodDistance(p) : hairDistance(p);
        if (dh < nearest + 0.004) mat = hood ? M_JACKET : M_HAIR; // hair (or the hood) covers the scalp
        d = smin(d, dh, 0.008);
        if (hat > 0 && !hood) { // a hat over the hair
            float dhat = hatDistance(p);
            if (dhat < min(nearest, dh) + 0.003) mat = M_HAT;
            d = min(d, dhat);
        }
    }
    return d;
}

vec3 bodyNormal(vec3 p) {
    const vec2 e = vec2(1.0, -1.0) * 0.0015;
    int m;
    return normalize(e.xyy * body(p + e.xyy, m) + e.yyx * body(p + e.yyx, m) +
                     e.yxy * body(p + e.yxy, m) + e.xxx * body(p + e.xxx, m));
}

// ---------- Paint details ----------

vec3 faceDetail(vec3 base, vec3 p) {
    vec3 q = toHead(p);
    if (q.z < 0.03) return base;
    float mouthOpen = face.x, eyelids = face.y, brow = face.z, smile = expression.x, gaze = expression.y;

    // Blush
    vec2 cheek = vec2(abs(q.x) - 0.045, q.y + 0.030);
    base = mix(base, vec3(0.90, 0.50, 0.45), 0.15 * (1.0 - smoothstep(0.0, 0.025, length(cheek))));

    // Eyes: white, iris, pupil, upper lid
    vec2 e = vec2(abs(q.x) - 0.033, q.y + 0.008);
    float lid = (e.x * e.x) / (0.016 * 0.016) + (e.y * e.y) / (0.0085 * 0.0085);
    if (lid < 1.0) {
        vec2 iris = e - vec2(0.0, gaze * 0.003);
        base = vec3(0.95, 0.92, 0.86);
        if (length(iris) < 0.0068) base = vec3(0.26, 0.19, 0.13);
        if (length(iris) < 0.0030) base = vec3(0.05);
        if (e.y > 0.0 && lid > 0.55) base = vec3(0.12, 0.08, 0.08);
        // Eyelid comes down from the top, ending in a dark lash line
        float lidEdge = 0.0085 - 2.0 * 0.0085 * eyelids;
        if (e.y > lidEdge) base = skinColor * 0.88;
        if (eyelids > 0.05 && abs(e.y - lidEdge) < 0.0012) base = vec3(0.12, 0.08, 0.08);
    }

    // Brows: raised, or inner ends up when shy
    float browY = 0.0195 + 0.004 * max(brow, 0.0) + 0.3 * min(brow, 0.0) * e.x;
    if (abs(e.x) < 0.02 && abs(e.y - browY + 18.0 * e.x * e.x) < 0.0032)
        base = hairColor * 0.8;

    // Mouth: a curved line, or open
    float curve = 3.0 + 7.0 * smile;
    vec2 m = vec2(q.x / 0.017, (q.y + 0.054) / (0.003 + 0.011 * mouthOpen));
    if (mouthOpen > 0.05 && dot(m, m) < 1.0)
        base = vec3(0.30, 0.10, 0.10);
    else if (abs(q.x) < 0.021 && abs(q.y + 0.052 - curve * q.x * q.x) < 0.0026)
        base = mix(skinColor, vec3(0.65, 0.25, 0.25), 0.6);

    return base;
}

vec3 hairDetail(vec3 base, vec3 p) {
    vec3 q = toHead(p);
    // Strands run down from the crown: noise varies around the head, not along it
    float around  = atan(q.x, q.z);
    float strands = valueNoise(vec3(around * 9.0, q.y * 1.2, 0.0)) * 0.6
                  + valueNoise(vec3(around * 23.0, q.y * 2.0, 3.0)) * 0.4;
    base *= 0.78 + 0.4 * strands;
    if (q.y > 0.06 && abs(q.x - 0.022) < 0.004) base *= 0.7; // parting
    return base;
}

vec3 clothDetail(int mat, vec3 base, vec3 p) {
    if (mat == M_PANTS) {
        vec3  rel  = p - joints[J_PELVIS];
        float belt = abs(dot(rel, spineDir) - 0.06);
        vec3  side = cross(spineDir, chestForward);
        if (belt < 0.018) base = vec3(0.25, 0.17, 0.12);
        if (belt < 0.012 && abs(dot(rel, side)) < 0.02 && dot(rel, chestForward) > 0.0) base = vec3(0.75, 0.70, 0.50);
        // standing to pee: the fly open (underwear showing in the slit)
        float along = dot(rel, spineDir);
        if (undress < -0.5 && garment <= 1 && along < 0.04 && along > -0.07 && dot(rel, chestForward) > 0.03 &&
            abs(dot(rel, side)) < 0.012 + 0.012 * smoothstep(-0.07, 0.03, along)) base = vec3(0.90, 0.90, 0.87);
    } else if (mat == M_SHIRT) {
        // Collar: a band around the base of the neck
        vec3  rel   = p - joints[J_CHEST];
        float along = dot(rel, spineDir);
        float ring  = length(rel - spineDir * along - chestForward * 0.005);
        if (along > 0.05 && ring > 0.04 && ring < 0.075) base *= 0.8;
    } else if (mat == M_SHOES) {
        if (p.y < 0.03) base *= 0.55; // sole
    }
    return base;
}

vec3 materialColor(int mat) {
    if (mat == M_HAIR)  return hairColor;
    if (mat == M_HAT)   return hatColor;
    if (mat == M_CAST)  return vec3(0.94, 0.93, 0.88);
    if (mat == M_SHIRT) return shirtColor;
    if (mat == M_PANTS) return pantsColor;
    if (mat == M_SHOES) return shoeColor;
    if (mat == M_PEE)   return vec3(0.96, 0.86, 0.38);
    if (mat == M_SICK)  return vec3(0.70, 0.66, 0.30);
    if (mat == M_WATER) return vec3(0.78, 0.90, 0.96);
    if (mat == M_CIG)   return vec3(0.96, 0.95, 0.92);
    if (mat == M_JACKET) return jacketColor;
    if (mat == M_URINELLA) return vec3(0.55, 0.78, 0.92);
    if (mat == M_FUR_DARK) return hairColor;
    if (mat == M_NOSE)  return vec3(0.08, 0.06, 0.05);
    return skinColor;
}

// ---------- Main ----------

void main() {
    const vec3 UP = vec3(0.0, 1.0, 0.0);
    vec3 R = normalize(cross(facing, UP)); // figure's right

    // World -> figure space (x = right, y = up, z = forward)
    #define TO_FIGURE_DIR(v) vec3(dot(v, R), (v).y, dot(v, facing))

    // The ray through this pixel: from the camera, or along the light
    vec3 dirWorld, startWorld;
    float travel;
    if (shadowPass) {
        dirWorld   = -normalize(lightDirection);
        startWorld = CardWorld - dirWorld * 1.5;
        travel     = 3.0;
    } else {
        vec3 toCard = CardWorld - viewPos;
        float dist  = length(toCard);
        dirWorld    = toCard / dist;
        float skip  = max(dist - 1.5, 0.0);
        startWorld  = viewPos + dirWorld * skip;
        travel      = dist + 1.5 - skip;
    }
    // Hand-drawn edge tremble, stepped with the boil clock
    vec3 tremble = (vec3(valueNoise(vec3(CardWorld.xy * 9.0, boilTime * 3.0)),
                         valueNoise(vec3(CardWorld.yz * 9.0, boilTime * 3.0 + 7.1)),
                         valueNoise(vec3(CardWorld.zx * 9.0, boilTime * 3.0 + 3.3))) - 0.5) * 0.008;
    vec3 origin = TO_FIGURE_DIR(startWorld - figurePos) + tremble;
    vec3 ray    = TO_FIGURE_DIR(dirWorld);

    headPos  = joints[J_HEAD];
    headF    = normalize(headForward);
    headU    = normalize(headUp);
    headR    = cross(headU, headF); // figure space is x right, y up, z forward
    spineDir = normalize(joints[J_CHEST] - joints[J_PELVIS]);
    buildBody();

    // Keep only parts whose bounding sphere the ray passes
    for (int i = 0; i < partCount; i++) {
        vec3  c = (partA[i] + partB[i]) * 0.5;
        float r = length(partB[i] - partA[i]) * 0.5 + max(partR1[i], partR2[i]) + partBlend[i] + 0.01;
        if (length(cross(c - origin, ray)) < r) candidates |= 1 << i;
    }
    vec3 head = onHead(vec3(0.0, hairStyle == 1 ? -0.08 : 0.0, -0.02));
    hairCandidate = species == 0 && length(cross(head - origin, ray)) < (hairStyle == 1 || hood ? 0.22 : hat == 1 ? 0.2 : 0.15);
    if (candidates == 0 && !hairCandidate) discard;

    // March the ray through the body
    float t = 0.0;
    int mat = M_SKIN;
    bool hit = false;
    for (int i = 0; i < 64; i++) {
        float d = body(origin + ray * t, mat);
        if (d < 0.0012) { hit = true; break; }
        t += d * 0.9;
        if (t > travel) break;
    }
    if (!hit) discard;

    vec3 p     = origin + ray * t;
    vec3 world = figurePos + R * p.x + UP * p.y + facing * p.z;
    vec4 clip  = viewProj * vec4(world, 1.0);
    gl_FragDepth = clip.z / clip.w * 0.5 + 0.5;
    if (shadowPass) return;

    vec3 nl = bodyNormal(p);
    vec3 n  = normalize(R * nl.x + UP * nl.y + facing * nl.z);

    vec3 base = materialColor(mat);
    if (mat == M_SKIN && species == 0) base = faceDetail(base, p);
    if (species == 1 && mat == M_FUR) { // her eyes, a darker back
        for (int s = 0; s < 2; s++) {
            vec3 e = toHead(p) - vec3(s == 0 ? -0.030 : 0.030, 0.022, 0.050);
            if (dot(e, e) < 0.012 * 0.012) base = vec3(0.07, 0.05, 0.04);
        }
        base *= 1.0 - 0.18 * smoothstep(0.02, 0.07, p.y - joints[J_PELVIS].y);
    }
    else if (mat == M_HAIR) base = hairDetail(base, p);
    else if (mat < M_PEE) base = clothDetail(mat, base, p);
    if (mat == M_HAT && hat == 2) base *= 0.9 + 0.1 * step(0.5, fract(dot(toHead(p), vec3(60.0, 0.0, 0.0)) + 0.25)); // (knitted ribs)
    if (species == 0 && (mat == M_SHIRT || mat == M_PANTS || mat == M_JACKET || mat == M_HAIR || mat == M_SHOES || mat == M_HAT))
        base *= 1.0 - 0.34 * soaked; // (wet cloth: darker)
    if (mat == M_JACKET) { // the zip down the front, a darker collar
        vec3 rel = p - joints[J_CHEST];
        vec3 side = cross(spineDir, chestForward);
        float along = dot(rel, spineDir);
        if (dot(rel, chestForward) > 0.0 && abs(dot(rel, side)) < 0.006) base *= 0.6;
        if (along > 0.04) base *= 0.85;
    }
    float ember = 0.0;
    if (mat == M_CIG) { // filter at the hand end, ash and ember at the other
        float along = dot(p - cigA, normalize(cigTip - cigA)), left = length(cigTip - cigA) - along;
        if (along < 0.02) base = vec3(0.85, 0.58, 0.32);
        if (left < 0.006) { base = mix(vec3(0.55, 0.53, 0.5), vec3(1.0, 0.35, 0.08), step(0.05, cigGlow)); ember = cigGlow; }
    }

    vec3  l       = normalize(lightDirection);
    // Step towards the light first, so tiny features (nose, chin) don't shadow the face
    vec4  lightSpacePos = lightSpaceMatrix * vec4(world + n * 0.03 + l * SELF_SHADOW_SKIP, 1.0);
    float visible = mix(1.0, shadowVisibility(shadowMap, lightSpacePos, n, l), shadowStrength);
    vec4  paint   = paintAlbedo(base, world, boilTime * 0.3);
    vec3  color   = paintLight(mix(base, paint.rgb, 0.25), n, l, visible, world, boilTime,
                               lightColor, shadowColor, skyTop, FIGURE_LIGHT_WRAP);
    color += fireGlow(base, n, world);
    color += vec3(1.0, 0.4, 0.1) * ember * 1.5; // (a drag: it glows)
    color = applyFog(color, length(viewPos - world), skyHorizon);

    OutColor       = vec4(color, 1.0);
    OutNormalDepth = vec4(normalize(mat3(view) * n), -(view * vec4(world, 1.0)).z);
}