Explanation

I wanted to do something 3D and simple, so I went with ray marching, which is a relatively simple algorithm to implement that offers a lot of possibilities. Following Inigo Quilez's work and my past experimentation, I was able to come up with a simple scene, apply lighting, and eventually add oscillating effects to both the motion and color of the scene. Lighting was fun to implement this time because I actually knew what I was doing instead of just copy pasting from LearnOpenGL.

I stuck to just three primitives and an infinite ground plane, all reflective, with the primitives oscillating around a point. On mouse down, the anchor of rotation becomes the mouse position. The colors use a cosine palette based on the ideas from Inigo Quilez's palettes article. As for functions: sine, cosine, pow, clamp, length, normalize, reflect, dot, min, max, etc. I could have used mix and step, but I found his GPU conditionals article. (It is a really good read, though I am not sure if it applies to WGSL.)

Reaction

My classmate was impressed that I could get a ray marcher down to around 60-ish lines.(In the initial version, I added the torus and tetrahedron later.) Overall, it was artistically simple. I do admit it is lacking in creativity and I got too caught up in doing something technical instead.

video

code

link to TheSchwartz

also served on this site at ./a2/a2.wgsl

// refs: book of shaders, iquilezles, most of the raymarcher was converted from an old project in common-lisp
// https://iquilezles.org/articles/distfunctions/
// https://iquilezles.org/articles/normalsSDF/

// raymarcher
const camera       = vec3(0., 0., -3.);
const iterations   = 90;
const max_distance = 100.0;
const min_distance = 0.01;
const fov          = 0.9;
const ground_shift = 0.9;
const damping      = 0.8;
const background   = vec3(135./255., 206./255., 235./255.);

// 2pi/3 4pi/3, 120deg
const _2pi_3 = 2.094; //2 * PI / 3;
const _4pi_3 = 4.189; //4* PI / 3;

fn sdTorus( p: vec3f, t: vec2f ) -> f32
{
  let q = vec2(length(p.xz)-t.x,p.y);
  return length(q)-t.y;
}

fn sdOctahedron( _p: vec3f,  s:f32 ) -> f32
{
  let p = abs(_p);
  let m = p.x+p.y+p.z-s;
  var q = vec3(0.);
  if( 3.0*p.x < m ) { q = p.xyz; }
  else if( 3.0*p.y < m ) { q = p.yzx; }
  else if( 3.0*p.z < m ) { q = p.zxy; }
  else {  return m*0.57735027; }

  let k = clamp(0.5*(q.z-q.y+s),0.0,s);
  return length(vec3(q.x,q.y-s+k,q.z-k));
}

fn sdSphere(pos: vec3f, r: f32) -> f32 {
  return length(pos) - r;
}

// spheres orbit in a triangle
fn scene_map(ray: vec3f) -> f32 {
  let t  = frame * 0.03;
  let r  = 0.6;
  let origin = select(vec3(0., 0., 0.), vec3(mouse.x, -mouse.y, 0.), mouse.z > 0.5);
  let p0 = origin + vec3(r * cos(t),         sin(t * 0.7),      r * sin(t));
  let p1 = origin + vec3(r * cos(t + _2pi_3), sin(t * 0.7 + 1.), r * sin(t + _2pi_3));
  let p2 = origin + vec3(r * cos(t + _4pi_3), sin(t * 0.7 + 2.), r * sin(t + _4pi_3));
  let ground = ground_shift + ray.y;

  let blobs  = min(min(sdSphere(ray - p0, 0.55),
        sdOctahedron(ray - p1, 0.55)),
      sdTorus((ray - p2) * mat3x3(
          vec3(1.0, 0.0, 0.0),
          vec3(0.0,  0,   1.),
          vec3(0.0, -1.,   0.),
          ), vec2(0.4, 0.1)));
  return min(ground, blobs);
}

// central-diff normals
fn normal(p: vec3f) -> vec3f {
  let e = vec2(0.001, 0.);
  return normalize(vec3(
        scene_map(p + e.xyy) - scene_map(p - e.xyy),
        scene_map(p + e.yxy) - scene_map(p - e.yxy),
        scene_map(p + e.yyx) - scene_map(p - e.yyx)
        ));
}

fn march(raydir: vec3f) -> vec3f {
  var td = 0.0;
  for(var i = 0; i < iterations; i++) {
    let p = camera + raydir * td;
    let d = scene_map(p);
    if(d < min_distance) {
      // phong
      let n    = normal(p);
      let lp   = vec3(2., 4., -2.); // TODO move this
      let lv   = normalize(lp - p);
      let dif  = max(dot(n, lv), 0.);
      let ref_ = reflect(-lv, n);
      let spec = pow(max(dot(ref_, -raydir), 0.), 32.);
      // cosine palette: https://iquilezles.org/articles/palettes/
      let t   = frame * 0.02;
      let col = 0.5 + 0.5 * cos(t + n.xzy + vec3(0., _2pi_3, _4pi_3));
      return col * (0.1 + 0.8 * dif) + vec3(spec * 0.6);
    }
    if(td > max_distance) { return background; }
    td += d * damping;
  }
  return background;
}

@fragment
fn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {
  let _uv    = uv(pos.xy);
  let raydir = normalize(vec3(_uv.x * fov, -_uv.y * fov, 1.));
  let color  = march(raydir);
  return vec4(color, 1.);
}