The Recognizer +better shadows

A piece raymarching a recognizer (Tron, 1982) made out of Signed Distance Functions, with soft shadows and supersampling anti-aliasing. A tribute to Ken Perlin and to the movie that made me discover my passion for computer graphics.

Created by marcogomez on Mon, 15 Nov 2021 08:18:17 GMT.


#version 300 es
// ╔═════════════╦═════════════════╦════════════════╗
// ║ Marco Gomez ║ @TheCodeTherapy ║ https://mgz.me ║
// ╚═════════════╩═════════════════╩════════════════╝
precision highp float;

uniform sampler2D prgm2Texture;
uniform vec2 resolution;
uniform float time;

out vec4 fragColor;

const float PI = acos(-1.0);
const float TAU = PI * 2.0;

float gaussian(float z, float u, float o) {
  return (
    (1.0 / (o * sqrt(TAU))) *
    (exp(-(((z - u) * (z - u)) / (2.0 * (o * o)))))
  );
}

vec3 gaussGrain(float t) {
  vec2 ps = vec2(1.0) / resolution.xy;
  vec2 uv = gl_FragCoord.xy * ps;
  float seed = dot(uv, vec2(12.9898, 78.233));
  float noise = fract(sin(seed) * 43758.5453123 + t);
  noise = gaussian(noise, 0.0, 0.5);
  return vec3(noise);
}

void main(void) {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 tex = texture(prgm2Texture, uv);
  float frameScale = 29.97;
  float frameTime = floor(time * frameScale) / frameScale;
  vec3 grain = gaussGrain(frameTime) * 0.025;
  tex.rgb -= grain;
  float fadeIn = smoothstep(0.0, 2.0 * 2.125, time);
  fragColor = tex * fadeIn;
}

#version 300 es
// ╔═════════════╦═════════════════╦════════════════╗
// ║ Marco Gomez ║ @TheCodeTherapy ║ https://mgz.me ║
// ╚═════════════╩═════════════════╩════════════════╝
precision highp float;

uniform sampler2D eTexture0; // https://i.imgur.com/s1sWDwl.png
uniform vec2 resolution;
uniform vec2 mouselerp;
uniform float time;

out vec4 fragColor;

#define antialias 0 // change to 1 to enable super-sampling anti-aliasing
#define softshadow 1 // change to 1 to render the soft shadow
#define showReference 0 // change to 1 to show the image used as a reference to code the signed distance functions
#define marchSteps 64

const int materialGround = 1;
const int materialRecognizer = 2;

const float PI = acos(-1.0);
const float TAU = PI * 2.0;
const float SQRT3 = sqrt(3.0);
const float COS30 = SQRT3 / 2.0;
const float TAN30 = 1.0 / SQRT3;

mat4 rotationMatrix(vec3 axis, float angle) {
  axis = normalize(axis);
  float s = sin(angle);
  float c = cos(angle);
  float oc = 1.0 - c;
  return mat4(
    oc * axis.x * axis.x + c,          oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,
    oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c,          oc * axis.y * axis.z - axis.x * s, 0.0,
    oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c,          0.0,
    0.0,                               0.0,                               0.0,                               1.0
  );
}

vec3 rotate(vec3 v, vec3 axis, float angle) {
  mat4 m = rotationMatrix(axis, angle);
  return (m * vec4(v, 1.0)).xyz;
}

float boxSDF(vec3 p, vec3 b) {
  vec3 q = abs(p) - b;
  return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}

float halfBoxSDF(vec3 p, vec3 b, float cutRotation) {
  const vec3 zAxis = vec3(0.0, 0.0, 1.0);
  float base = boxSDF(p, b);
  vec3 cut = rotate(p, zAxis, cutRotation);
  return clamp(max(cut.x, base), -1.0, 0.5);
  // return max(cut.x, base);
}

float parallelogramSDF(vec2 p, float wi, float he, float sk) {
  vec2 e = vec2(sk, he);
  p = (p.y < 0.0) ? -p : p;
  vec2 w = p - e;
  w.x -= clamp(w.x, -wi, wi);
  vec2 d = vec2(dot(w, w), -w.y);
  float s = p.x * e.y - p.y * e.x;
  p = (s < 0.0) ? -p : p;
  vec2 v = p - vec2(wi,0);
  v -= e * clamp(dot(v, e) / dot(e, e), -1.0, 1.0);
  d = min(d, vec2(dot(v, v), wi * he - abs(s)));
  return sqrt(d.x) * sign(-d.y);
}

float hexagonalSDF(vec3 p, vec2 h, float xMult) {
  const vec3 k = vec3(-COS30, 0.2, 1e4); //TAN30);
  p = abs(vec3(p.x * xMult, p.y, p.z));
  p.xy -= 2.0 * min(dot(k.xy, p.xy), 0.0) * k.xy;
  vec2 d = vec2(
    length(p.xy - vec2(clamp(p.x, -k.z * h.x, k.z * h.x), h.x)) * sign(p.y - h.x),
    p.z - h.y
  );
  return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}

float pyramidalSDF(vec3 p, float h, float sMult) {
  p /= sMult;
  float m2 = h * h + 0.25;
  p.xz = abs(p.xz);
  p.xz = (p.z > p.x) ? p.zx : p.xz;
  p.xz -= 0.5;
  vec3 q = vec3(p.z, h * p.y - 0.5 * p.x, h * p.x + 0.5 * p.y);
  float s = max(-q.x, 0.0);
  float t = clamp((q.y - 0.5 * q.x) / (m2 + 0.25), 0.0, 1.0);
  float a = m2 * (q.x + s) * (q.x + s) + q.y * q.y;
  float b = m2 * (q.x + 0.5 * t) * (q.x + 0.5 * t) + (q.y - m2 * t) * (q.y - m2 * t);
  float d2 = max(-q.y, q.x * m2 + q.y * 0.5) < 0.0 ? 0.0 : min(a, b);
  return sqrt((d2 + q.z * q.z) / m2) * sign(max(q.z, -p.y));
}

float legsSDF(vec3 p) {
  vec3 absp = vec3(abs(p.x), p.y, p.z);
  float legA =     boxSDF(absp + vec3(-1.33, -1.00, 0.0), vec3(0.20, 0.55, 0.2)                );
  float legB =     boxSDF(absp + vec3(-1.33, -2.00, 0.0), vec3(0.20, 0.20, 0.2)                );
  float feet = halfBoxSDF(absp + vec3(-0.97, -0.36, 0.0), vec3(0.56, 0.10, 0.2), radians(120.0));
  float legs = min(legA, legB);
  legs = min(legs, feet) - 0.005;
  return legs;
}

float torsoSDF(vec3 p) {
  vec3 absp = vec3(abs(p.x), p.y, p.z);
  float hipCenter =              boxSDF(        p + vec3(  0.0, -1.67, 0.0), vec3(1.53, 0.050, 0.2)                );
  float hipTop =                 boxSDF(        p + vec3(  0.0, -1.79, 0.0), vec3(0.30, 0.035, 0.2)                );
  float hipBottom =          halfBoxSDF(     absp + vec3(-0.26, -1.55, 0.0), vec3(0.27, 0.035, 0.2), radians(-45.0));
  float chestCenter =        halfBoxSDF(     absp + vec3(-0.39, -2.12, 0.0), vec3(0.40, 0.120, 0.2), radians(-45.0));
  float chestRibsLim =           boxSDF(        p + vec3(  0.0, -2.09, 0.0), vec3(1.10, 0.235, 0.2)                );
  float chestRibsSDF = parallelogramSDF(vec3(absp + vec3(-0.55, -2.00, 0.0)).xy,   0.2, 0.45,         radians(25.0));
  float chestBottom =            boxSDF(     absp + vec3(-0.17, -1.90, 0.0), vec3(0.13, 0.045, 0.2)                );
  float chestTop =               boxSDF(     absp + vec3(-0.85, -2.34, 0.0), vec3(0.81, 0.045, 0.2)                );
  float arms =                   boxSDF(     absp + vec3(-0.94, -2.00, 0.0), vec3(0.12, 0.050, 0.2)                );
  float chestRibs = max(chestRibsLim, chestRibsSDF);
  float hip = min(hipCenter, hipTop);
  hip = min(hip, hipBottom);
  float chest = min(chestCenter, chestRibs);
  chest = min(chest, chestBottom);
  chest = min(chest, chestTop);
  float torso = min(hip, chest);
  torso = min(torso, arms) - 0.005;
  return torso;
}

float headSDF(vec3 p) {
  float headLimit =       boxSDF(p + vec3(  0.0, -2.58, 0.0), vec3(0.90, 0.15, 0.2) );
  float headShape = hexagonalSDF(p + vec3(  0.0, -2.43, 0.0), vec2(0.32, 0.20), 1.0 );
  float headHole =        boxSDF(p + vec3(  0.0, -2.64, 0.0), vec3(0.65, 0.03, 0.07));
  float headZCutA = pyramidalSDF(p + vec3(0.0, -2.4,  0.8), 1.7, 2.0);
  float headZCutB = pyramidalSDF(p + vec3(0.0, -2.4, -0.8), 1.7, 2.0);
  float head = max(headShape, headLimit);
  head = max(head, -headHole);
  head = max(head, headZCutA);
  head = max(head, headZCutB);
  return head - 0.005;
}

float bodySDF(vec3 p) {
  float legs = legsSDF(p);
  float torso = torsoSDF(p);
  float head = headSDF(p);
  float body = min(legs, torso);
  body = min(body, head);
  return body;
}

vec3 rotateBody(vec3 q) {
  float t = time * 0.25;
  q.y -= 0.35;
  q.xy += vec2(cos(t), sin(t) * cos(t));
  q = rotate(q, vec3(0.0, 0.0, 1.0), sin(time * 0.125) * 0.0625);
  q = rotate(q, vec3(0.0, 1.0, 0.0), sin(time * 0.250) * 0.2);
  return q;
}

float mapDistance(vec3 p) {
  #if showReference == 0
    vec3 q = rotateBody(p);
    float body = bodySDF(q);
  #else
    float body = bodySDF(p);
  #endif
  float groundDist = p.y + 0.38;
  float dist = min(body, groundDist);
  return dist;
}

int mapMaterial(vec3 p) {
  #if showReference == 0
    vec3 q = rotateBody(p);
    float body = bodySDF(q);
  #else
    float body = bodySDF(p);
  #endif
  float groundDist = p.y;
  float dist = min(body, groundDist);
  int material = 0;
  if (dist == groundDist) {
    material = materialGround;
  } else if (dist == body) {
    material = materialRecognizer;
  }
  return material;
}

float rayMarch(vec3 ro, vec3 rd) {
  float dist = 0.0;
  for (int i = 0; i < marchSteps; i++) {
    vec3 p = ro + rd * dist;
    float sceneDist = mapDistance(p);
    dist += sceneDist;
    if (dist > 40.0 || sceneDist <= 0.003) { break; }
  }
  return dist;
}

vec3 getNormal(vec3 p) {
  float dist = mapDistance(p);
  vec2 closeSample = vec2(0.01, 0.0);
  vec3 closeSampleV3 = vec3(
    mapDistance(p - closeSample.xyy),
    mapDistance(p - closeSample.yxy),
    mapDistance(p - closeSample.yyx)
  );
  vec3 normal = dist - closeSampleV3;
  return normalize(normal);
}

float getAmbientOcclusion(vec3 pos, vec3 nor) {
  float occ = 0.0;
  float sca = 1.0;
  for (int i = 0; i < 5; i++) {
    float h = 0.001 + 0.25 * float(i) / 4.0;
    float d = mapDistance(pos + h * nor);
    occ += (h - d) * sca;
    sca *= 0.98;
  }
  return clamp(1.0 - 1.6 * occ, 0.0, 1.0);
}

float getSoftShadow(vec3 ro, vec3 rd, float tmin, float tmax, float k) {
  float res = 1.0;
  float ph = 1e20;
  for(float t = tmin; t < tmax; ) {
    float h = mapDistance(ro + rd * t);
    if (h < 0.001) { return 0.0; }
    float y = h * h / (2.0 * ph);
    float d = sqrt(h * h - y * y);
    res = min(res, k * d / max(0.0, t-y));
    ph = h;
    t += h;
  }
  return res;
}

vec3 getCameraPosition(void) {
  vec3 ro = vec3(0.0);
  #if showReference == 1
    ro = vec3(0.0, 1.5, 6.0);
  #else
    const float camHeight = 2.5;
    const float camDistanceRadius = 6.0;
    float camPosX = sin(mouselerp.x * PI * 1.5) * camDistanceRadius;
    float camPosY = clamp(camHeight + mouselerp.y * PI, 1.5, 7.0);
    float camPosZ = cos(mouselerp.x * PI * 1.5) * camDistanceRadius;
    ro = vec3(camPosX, camPosY, camPosZ);
    float t = time * 0.125;
    ro.xy += vec2(cos(t), sin(t) * cos(t)) * 2.0;
  #endif
  return ro;
}

mat3 calcLookAtMatrix(vec3 origin, vec3 target, float roll) {
  vec3 rr = vec3(sin(roll), cos(roll), 0.0);
  vec3 ww = normalize(target - origin);
  vec3 uu = normalize(cross(ww, rr));
  vec3 vv = normalize(cross(uu, ww));
  return mat3(uu, vv, ww);
}

vec3 getCameraTarget(vec2 uv, vec3 ro) {
  vec3 camTarget = vec3(0.0, 2.5, 0.0);
  float fov = 1.5; float camRoll = 0.0;
  #if showReference == 0
    camRoll = 0.0 + sin(time * 0.25) * 0.0625;
    fov = 1.2;
    vec3 q = camTarget;
    float t = time * 0.25;
    q.y -= 0.35;
    q.xy -= vec2(cos(t), sin(t) * cos(t)) * 0.5;
    camTarget = q;
  #endif
  mat3 camMatrix = calcLookAtMatrix(ro, camTarget, camRoll);
  vec3 rd = normalize(camMatrix * vec3(uv.x, uv.y, fov));
  return rd;
}

vec4 render(vec2 uv) {
  float ambient = 0.3;
  float spec = 0.3;
  vec3 col = vec3(0.0);
  vec3 baseMat = vec3(0.2, 0.5, 1.0);

  vec3 ro = getCameraPosition();
  vec3 rd = getCameraTarget(uv, ro);

  float dist = rayMarch(ro, rd);
  vec3 p = ro + rd * dist;
  vec3 normal = getNormal(p);
  int material = mapMaterial(p);

  vec3 light = normalize(vec3(0.7, 1.5, 4.0));
  float l = clamp(dot(light, normal), 0.0, 1.0);
  vec3 ref = normalize(reflect(rd, normal));
  float r = clamp(dot(ref, light), 0.0, 1.0);
  float occlusion = getAmbientOcclusion(p, normal) * 0.7;
  float shadow = 1.0;
  #if softshadow == 1
    shadow = getSoftShadow(p, light, 0.01, 10.0, 8.0);
  #endif

  if (material == materialGround) {
    vec3 q = vec3(p.x, p.y, p.z + time * 5.0);
    float w = 0.05;
    float sx = step(mod(q.x, 1.0), w);
    float sz = step(mod(q.z, 1.0), w);
    baseMat = mix(
      baseMat,
      baseMat + vec3(sx + sz - (sx * sz)) * 3.0 * 1.0 - length(p * 0.25),
      0.125
    ) * (baseMat + shadow);
  }

  col = (
    min(vec3(max(shadow, ambient)), max(l, ambient)) *
    max(occlusion, ambient) * baseMat + pow(abs(r), 64.0) * spec
  ) + occlusion * 0.3;
  col -= vec3(length(p * 0.125)) * 0.25;
  col += baseMat * vec3(occlusion) * 0.3;

  return vec4(col, 1.0) * 1.2;
}

vec4 superSample(vec2 uv, float res, int steps, float offset) {
  vec4 rv = vec4(0.0);
  vec2 stepSize = vec2(offset) / res / float(steps);
  uv -= vec2(0.5) / res;
  for (int x = 0; x < steps; ++x) {
    for (int y = 0; y < steps; ++y) {
      vec2 off = vec2(float(x), float(y)) + vec2(0.5);
      rv += render(uv + off * stepSize);
    }
  }
  return rv / float(steps * steps);
}

float gaussian(float z, float u, float o) {
  return (
    (1.0 / (o * sqrt(TAU))) *
    (exp(-(((z - u) * (z - u)) / (2.0 * (o * o)))))
  );
}

vec3 gaussGrain(float t) {
  vec2 ps = vec2(1.0) / resolution.xy;
  vec2 uv = gl_FragCoord.xy * ps;
  float seed = dot(uv, vec2(12.9898, 78.233));
  float noise = fract(sin(seed) * 43758.5453123 + t);
  noise = gaussian(noise, 0.0, 0.5);
  return vec3(noise);
}

void main(void) {
  vec2 uv = (gl_FragCoord.xy - resolution.xy * 0.5) / resolution.y;
  float minRes = min(resolution.x, resolution.y);
  #if antialias == 1
    vec4 color = superSample(uv, minRes, 2, 1.25);
  #else
    vec4 color = render(uv);
  #endif
  vec3 grain = gaussGrain(time) * 0.03;
  color.xyz -= grain;
  fragColor = color;
  #if showReference == 1
    vec2 suv = gl_FragCoord.xy / resolution.xy;
    suv -= 0.5; suv *= vec2(1.5, 1.35); suv += 0.5;
    vec4 reference = texture(eTexture0, suv);
    fragColor = mix(fragColor, reference, reference.r);
  #endif
}

#version 300 es
// ╔═════════════╦═════════════════╦════════════════╗
// ║ Marco Gomez ║ @TheCodeTherapy ║ https://mgz.me ║
// ╚═════════════╩═════════════════╩════════════════╝
precision highp float;

uniform sampler2D prgm1Texture;
uniform vec2 resolution;
uniform float time;
uniform float fft;

out vec4 fragColor;

const float amount = 1.0;
const float reinhardAmount = 0.4;
const float contrast = 1.0;
const float brightness = 1.5;
const float saturation = 0.8;
const vec2 vignetteSize = vec2(0.25, 0.25);
const float vignetteRoundness = 0.12;
const float vignetteMix = 1.0;
const float vignetteSmoothness = 0.42;
const float W = 1.2;
const float T = 7.5;

float filmicReinhardCurve(float x) {
  float q = (T * T + 1.0) * x * x;
  return q / (q + x + T * T);
}

vec3 filmicReinhard(vec3 c) {
  float w = filmicReinhardCurve(W);
  return vec3(
    filmicReinhardCurve(c.r),
    filmicReinhardCurve(c.g),
    filmicReinhardCurve(c.b)
  ) / w;
}

vec3 ContrastSaturationBrightness(vec3 color, float brt, float sat, float con) {
  const float AvgLumR = 0.5;
  const float AvgLumG = 0.5;
  const float AvgLumB = 0.5;
  const vec3 LumCoeff = vec3(0.2125, 0.7154, 0.0721);
  vec3 AvgLumin = vec3(AvgLumR, AvgLumG, AvgLumB);
  vec3 brtColor = color * brt;
  vec3 intensity = vec3(dot(brtColor, LumCoeff));
  vec3 satColor = mix(intensity, brtColor, sat);
  vec3 conColor = mix(AvgLumin, satColor, con);
  return conColor;
}

float sdSquare(vec2 point, float width) {
  vec2 d = abs(point) - width;
  return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}

float vignette(vec2 uv, vec2 size, float roundness, float smoothness) {
  uv -= 0.5;
  float minWidth = min(size.x, size.y);
  uv.x = sign(uv.x) * clamp(abs(uv.x) - abs(minWidth - size.x), 0.0, 1.0);
  uv.y = sign(uv.y) * clamp(abs(uv.y) - abs(minWidth - size.y), 0.0, 1.0);
  float boxSize = minWidth * (1.0 - roundness);
  float dist = sdSquare(uv, boxSize) - (minWidth * roundness);
  return 1.0 - smoothstep(0.0, smoothness, dist);
}

void main(void) {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 tex = texture(prgm1Texture, uv);
  vec3 reinhard = filmicReinhard(tex.rgb);
  vec3 color = tex.rgb;
  color = mix(color, reinhard, reinhardAmount);
  color = ContrastSaturationBrightness(color, brightness, saturation, contrast);
  float v = vignette(uv, vignetteSize, vignetteRoundness, vignetteSmoothness);
  vec3 vig = color * v;
  color = mix(color, vig, vignetteMix);
  color = mix(tex.xyz, color, amount);
  color = clamp(color, 0.0, 1.0);
  fragColor = vec4(color, 1.0);
}