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