// 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); }