The Code Therapy

Tracing Boilerplate

A simple starting point for simple tracing scenes

Created by marcogomez on Sun, 22 May 2022 09:36:48 GMT.


#version 300 es
precision highp float;

out vec4 fragColor;

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

void main(void) {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 tex = texture(prgm1Texture, uv);
  vec3 col = tex.xyz / tex.w;
  col = mix(col, pow(col, vec3(0.4545)), 0.5);
  fragColor = vec4(col, 1.0);
}

#version 300 es
precision highp float;

uniform sampler2D prgm1Texture;
uniform float time;
uniform vec2 resolution;
uniform vec4 mousedrag;

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

out vec4 fragColor;

const vec3 sunDir = normalize(vec3(0.8, 0.5, -1.0));
const vec3 sunCol = vec3(0.5, 0.7, 1.0);

struct Ray { vec3 ro; vec3 rd; };
struct Hit { int id; vec3 p; vec3 n; float t; };
struct Material { vec3 albedo; bool is_metal; float fuzz; bool isGlass; float refID; vec3 phong; };

float mtime = 0.0;

float sphereIntersect(Ray ray, vec4 sph) {
  vec3 c = sph.xyz;
  float r = sph.w;
  vec3 co = ray.ro - c;
  float b = dot(co, ray.rd);
  float d = b * b - dot(co, co) + r * r;
  if (d < 0.0) { return -1.0; }
  float sd = sqrt(d);
  vec2 t = vec2(-b - sd, -b + sd);
  if (t.x < 0.0) { return t.y; }
  return t.x;
}

float planeIntersect(Ray ray, vec3 n) {
  return -dot(ray.ro, n) / dot(ray.rd, n);
}

vec3 sphereNormal(vec3 p, vec4 sph) {
  return normalize(p - sph.xyz);
}

float sphereShadow(vec3 ro, vec3 rd, vec4 sph, float k) {
  vec3 oc = ro - sph.xyz;
  float b = dot(oc, rd);
  float c = dot(oc, oc) - sph.w * sph.w;
  float h = b * b - c;
  float d = -sph.w + sqrt(max(0.0, sph.w * sph.w - h));
  float t = -b - sqrt(max(0.0, h));
  return (t < 0.0) ? 1.0 : smoothstep(0.0, 1.0, k * d / t);
}

float fresnel(Ray ir, Hit hit, float eta) {
  float r0 = (1.0 - eta) / (1.0 + eta);
  r0 *= r0;
  float cos_t = dot(-ir.rd, hit.n);
  cos_t = clamp(cos_t, 0.0, 1.0);
  return r0 + (1.0 - r0) * pow(1.0 - cos_t, 5.0);
}

float hash21(vec2 p) {
  vec3 p3  = fract(vec3(p.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

vec2 hash22(vec2 p) {
  vec3 p3 = fract(vec3(p.xyx) * vec3(0.1031, 0.1030, 0.0973));
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.xx + p3.yz) * p3.zy);
}

float hash31(vec3 p) {
  p = fract(p * 0.1031);
  p += dot(p, p.zyx + 31.32);
  return fract((p.x + p.y) * p.z);
}

vec2 hash12(float p) {
  vec3 p3 = fract(vec3(p) * vec3(0.1031, 0.1030, 0.0973));
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.xx + p3.yz) * p3.zy);
}

vec3 sphereRND(vec3 p) {
  float f = hash31(p);
  float theta = TAU * hash21(vec2(f * 0.3482, f * 2.18622));
  float phi = acos(1.0 - 2.0 * hash21(vec2(f * 1.9013, f * 0.94312)));
  float x = sin(phi) * cos(theta);
  float y = sin(phi) * sin(theta);
  float z = cos(phi);
  return vec3(x,y,z);
}

vec4 spheres[] = vec4[](
  vec4(vec3(0.0), 0.7),
  vec4(vec3(0.0), 0.7),
  vec4(vec3(0.0), 0.7),
  vec4(vec3(0.0), 0.7),
  vec4(vec3(0.0), 0.7),
  vec4(vec3(0.0), 0.7)
);

// materials[id] is the material of the object with this id
const Material materials[] = Material[](
  Material(vec3(0.7, 0.7, 0.7),  true, 0.50, false, 0.0, vec3(1.0,  2.0, 10.0)),
  Material(vec3(0.5, 0.4, 0.3), false, 0.00, false, 0.0, vec3(1.0, 10.0, 10.0)),
  Material(vec3(1.0, 1.0, 1.0), false, 0.00,  true, 1.4, vec3(0.0,  1.0, 100.0)),
  Material(vec3(0.4, 0.7, 0.3),  true, 0.50, false, 0.0, vec3(1.0,  0.5, 50.0)),
  Material(vec3(0.8, 0.8, 0.8),  true, 0.20, false, 0.0, vec3(1.0,  2.0, 72.0)),
  Material(vec3(0.3, 0.2, 0.9), false, 0.00, false, 0.0, vec3(1.0,  0.0, 0.0)),
  Material(vec3(0.9, 0.3, 0.2),  true, 0.05, false, 0.0, vec3(1.0, 20.0, 1000.0))
);

void closest(float test_t, int test_id, inout float t, inout int id) {
  if (test_t > 0.0 && test_t < t) {
    t = test_t;
    id = test_id;
  }
}

bool sceneIntersect(Ray ray, inout Hit hit) {
  float angle = TAU / float(6);
  float t = 10000.0;
  int id = -1;
  vec3 groundNormal = vec3(0.0, 1.0, 0.0);
  closest(planeIntersect(ray, groundNormal), 0, t, id);
  for (int i = 0; i < 6; i++) {
    float bounce = fract(mousedrag.w * 0.8 + sin(float(i) / float(6) - 0.25)) - 0.5;
    spheres[i].xyz = vec3(
      cos(float(-i) * angle) * 2.0,
      spheres[i].w + 3.0 * (0.25 - bounce * bounce),
      sin(float(-i) * angle) * 2.0
    );
    vec4 sph = spheres[i];
    closest(sphereIntersect(ray, sph), i + 1, t, id);
  }
  vec3 p = ray.ro + ray.rd * t;
  vec3 n = groundNormal;
  if (id > 0) {
    n = sphereNormal(p, spheres[id - 1]);
  }
  hit = Hit(id, p, n, t);
  return id >= 0;
}

vec3 background(Ray ray) {
  vec3 ca = vec3(0.95, 0.65, 0.55);
  vec3 cb = vec3(0.3, 0.6, 1.0);
  float f = dot(ray.rd, vec3(0.0, 1.0, 0.0));
  f = max(0.0, f);
  f = pow(f, 0.3);
  vec3 col = mix(ca, cb, f);
  f = dot(ray.rd, sunDir);
  f = max(0.0, f);
  float k = smoothstep(0.9, 0.999, f * f);
  col += vec3(1.0, 0.8, 0.4) * k * 0.1;
  col += step(0.9997, f);
  return clamp(col, 0.0, 1.0);
}

float shadow(Hit hit) {
  Ray ray = Ray(hit.p + hit.n * 0.001, sunDir);
  float s = 1.0;
  for (int i = 0; i < 6; i++) {
    s *= sphereShadow(ray.ro, ray.rd, spheres[i], 12.0);
  }
  return s;
}

vec3 directLighting(Ray ir, Hit hit, Material mat, vec3 surfaceColor) {
  float kd = mat.phong.x;
  float ks = mat.phong.y;
  float sh = mat.phong.z;
  vec3 v = normalize(ir.ro - hit.p);
  vec3 r = reflect(-sunDir, hit.n);
  vec3 h = normalize(sunDir + v);
  float diff = kd * max(0.0, dot(sunDir, hit.n));
  float spec = ks * pow(max(0.0, dot(hit.n, h)), sh);
  float cos_t = dot(h, sunDir);
  vec3 r0 = mat.is_metal ? surfaceColor : vec3(0.1);
  vec3 fre = r0 + (1.0 - r0) * pow(abs(clamp(1.0 - cos_t, 0.0, 1.0)), 5.0);
  float shad = shadow(hit);
  vec3 col = vec3(0.0);
  if (!mat.is_metal) { col += surfaceColor * sunCol * diff * shad; }
  col += sunCol * diff * spec * shad * fre;
  return clamp(col, 0.0, 1.0);
}

Ray lambertScatter(Ray ir, Hit hit, Material mat) {
  vec3 ro, rd;
  rd = hit.n + sphereRND(hit.p + time);
  if (dot(rd, rd) < 0.001) { rd = hit.n; }
  rd = normalize(rd);
  ro = hit.p + hit.n * 0.001;
  return Ray(ro, rd);
}

Ray metalScatter(Ray ir, Hit hit, Material mat) {
  vec3 rd = reflect(ir.rd, hit.n);
  vec3 f = mat.fuzz * sphereRND(hit.p + time);
  rd = normalize(rd + f);
  vec3 ro = hit.p + hit.n * 0.001;
  return Ray(ro, rd);
}

Ray glassScatter(Ray ir, Hit hit, Material mat) {
  vec3 rd, ro;
  float eta = mat.refID;
  if (dot(ir.rd, hit.n) < 0.0) {
    eta = 1.0 / eta;
    if (fresnel(ir, hit, eta) > hash31(hit.p + time)) {
      rd = reflect(ir.rd, hit.n);
      ro = hit.p + hit.n * 0.001;
    } else {
      rd = refract(ir.rd, hit.n, eta);
      ro = hit.p - hit.n * 0.001;
    }
  } else { // inside the sphere
    rd = refract(ir.rd, -hit.n, eta);
    ro = hit.p + hit.n * 0.001;
  }
  return Ray(ro, rd);
}

float hash(vec2 uv) {
  return fract(sin(dot(uv.xy, vec2(12.9898, 78.233))) * 43758.5453123);
}

vec2 truchetPattern(vec2 uv, float index) {
  index = fract(((index - 0.5) * 2.0));
  if (index > 0.75) { uv = vec2(1.0) - uv; }
  else if (index > 0.50) { uv = vec2(1.0 - uv.x, uv.y); }
  else if (index > 0.25) { uv = 1.0 - vec2(1.0 - uv.x, uv.y); }
  return uv;
}

float curvedTruchet(vec2 uv) {
  vec2 ouv = uv;
  uv *= 1.0;
  vec2 ipos = floor(uv);
  vec2 fpos = fract(uv);
  vec2 tile = truchetPattern(fpos, hash(ipos));
  float a = (
    (step(length(tile), 0.6) - step(length(tile), 0.4)) +
    (step(length(tile - vec2(1.0)), 0.6) - step(length(tile - vec2(1.0)), 0.4))
  );
  float curves = a / length(ouv * 0.8) * 0.5;
  return clamp(curves, 0.0, 1.0);
}

vec3 surfaceTexture(Hit hit) {
  if (hit.id == 0) { // Checker texture
    vec2 uv = hit.p.xz;
    float ct = curvedTruchet(uv);
    return mix(vec3(0.1, 0.1, 0.3), vec3(1.0), ct);
  }
  return vec3(1.0);
}

vec3 render(Ray ray){ // color of one ray sample
  const int depth = 12; // number of rays
  vec3 colorMask = vec3(1.0);
  vec3 accumulatedColor = vec3(0.0);
  for (int i = 0; i < depth; i++) {
    Hit hit;
    if (sceneIntersect(ray, hit)) {
      Material mat = materials[hit.id];
      vec3 surfaceColor = mat.albedo * surfaceTexture(hit);
      accumulatedColor += directLighting(ray, hit, mat, surfaceColor * colorMask);
      colorMask *= surfaceColor;
      if (mat.is_metal) {
        ray = metalScatter(ray, hit, mat);
      } else if (mat.isGlass) {
        ray = glassScatter(ray, hit, mat);
      } else {
        ray = lambertScatter(ray, hit, mat);
      }
    } else {
      accumulatedColor += colorMask * background(ray);
      break;
    }
  }
  return accumulatedColor;
}

vec3 getCameraPosition(float mx, float my) {
  float acc = 0.0;
  const float camHeight = 0.5;
  const float camDistanceRadius = 6.5;
  float camPosX = sin(mx * PI * 2.0) * camDistanceRadius;
  float camPosY = camHeight + (my * 0.5 + 0.5) * 5.0;
  float camPosZ = cos(mx * PI * 2.0) * camDistanceRadius;
  vec3 ro = vec3(camPosX, camPosY, camPosZ);
  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, float aaSamples) {
  vec3 col = vec3(0.0);
  vec3 camTarget = vec3(0.0, 0.5, 0.0);
  float camRoll = 0.0; float fov = 1.25;
  mat3 camMatrix = calcLookAtMatrix(ro, camTarget, camRoll);
  for (float i = 0.0; i < aaSamples; i++) {
    vec2 rnd = (1.0 / resolution.x) * (-1.0 + 2.0 * hash12(i + time));
    vec3 rd = normalize(camMatrix * vec3(uv + rnd, fov));
    Ray ray = Ray(ro, rd);
    col += render(ray);
  }
  col /= aaSamples;
  return col;
}

void main(void) {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  vec4 tex = texture(prgm1Texture, uv);
  if (mousedrag.z == 1.0) { tex *= 0.0; }
  uv -= 0.5;
  uv.x *= resolution.x / resolution.y;
  vec3 ro = getCameraPosition(mousedrag.x, mousedrag.y);
  vec3 col = vec3(0.0);
  col += getCameraTarget(uv, ro, 8.0);
  col = clamp(col, 0.0, 1.0);
  tex += vec4(col, 1.0);
  fragColor = tex;
}