Entropie git · main
Entropie festival game; first steps
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.gzentropie/assets/shaders/figure.frag 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);
}