Cornell Box #1

Somebody had to...
I have created a path tracer to render the Cornell Box (en.wikipedia.org/wiki/cornell_box).
Ray trace code base on the (excellent) book "Ray tracing in one weekend" (in1weekend.blogspot.…-in-one-weekend.html) by Peter Shirley (@Peter_shirley).
The code is simplified for this specific scene, and I have added direct light sampling to reduce noise.

#raytracer #pixels #rays #dithering

Log in to post a comment.

// Cornell Box. Created by Reinder Nijhoff 2019
// @reindernijhoff
//
// https://turtletoy.net/turtle/50b8079a52
//
// Ray trace code base on the (excellent) book "Ray tracing in one weekend"[1] by Peter Shirley 
// (@Peter_shirley). 
//
// The code is simplified for this specific scene, and I have added direct light sampling to reduce noise.
//
// [1] http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html
//

const drawNegative = false; // toggle black on white or white on black

Canvas.setpenopacity(drawNegative?-1:1);

const MAX_RECURSION = 3;
const SAMPLES_PER_PIXEL = 2;

const canvas_size = 100;
const step_size = .5;
const line_seperation = .5;

const mask = [1/16, 1/2+1/32, 1/4-1/32, 1/2-1/32, 1/8, 1/2+1/32, 1/4, 1/2];
const turtles = [];

let cam;
let world = [];
let shadow_casters = [];
let lightSource;

//
// Init all turtles used for hatching
//
for (let i=-canvas_size;i<canvas_size; i+=line_seperation) {
    const t1 = new Turtle(-canvas_size,i);
    t1.seth(45); t1.mask = 0;
    turtles.push(t1);
}
for (let i=canvas_size;i>-canvas_size; i-=line_seperation) {
    const t2 = new Turtle(i,-canvas_size);
    t2.seth(45); t2.mask = 0;
    turtles.push(t2);
}
for (let i=-canvas_size;i<canvas_size; i+=line_seperation) {
    const t3 = new Turtle(canvas_size,i);
    t3.seth(135); t3.mask = 1/64;
    turtles.push(t3);
}
for (let i=-canvas_size;i<canvas_size; i+=line_seperation) {
    const t4 = new Turtle(i,-canvas_size);
    t4.seth(135); t4.mask = 1/64;
    turtles.push(t4);
}

function walk(i) {
    if (i==0) {
        init();
    }
    
    for (let j=0; j<turtles.length; j++) {
        const t = turtles[j];
        const x = t.x();
        const y = t.y();
        if (Math.abs(x) <= canvas_size && Math.abs(y) <= canvas_size) {
            if (get_image_intensity(x/canvas_size,y/canvas_size) < 
                                    .5 * (mask[j % 8] + t.mask) == drawNegative) {
                t.penup();
            } else {
                t.pendown();
            }
            t.forward(step_size);
        }
    }
    return i < 300/step_size;
}

function get_image_intensity(x,y) {
    let l = 0;
    for (let i=0; i<SAMPLES_PER_PIXEL; i++) {
        const r = cam.get_ray([x, y]);
        const col = color(r);
        l += dot3(col, [0.299, 0.587, 0.114]);
    }
    return Math.pow(l/SAMPLES_PER_PIXEL, 1/1.5);
}

//
// Init world
//
function init() {
    cam = new camera([278, 278, -800], [278,278,0], [0,1,0], 40, 1);
    
    const red = new material([.65,.05,.05], [0,0,0], false);
    const white = new material([.73,.73,.73], [0,0,0], false);
    const green = new material([.12,.45,.15], [0,0,0], false);
    const light = new material([0,0,0], [15,15,15], true);
    
    world.push(new hitable([556,277.5,277.5], [1,277.5,277.5], green));
    world.push(new hitable([-1,277.5,277.5], [1,277.5,277.5], red));
    world.push(new hitable([277.5,556,277.5], [277.5,1,277.5], white));
    world.push(new hitable([277.5,-1,277.5], [277.5,1,277.5], white));
    world.push(new hitable([277.5,277.5,556], [277.5,277.5,1], white));

    lightSource = new hitable([278,555,279.5], [65,1,52.5], light);
    world.push(lightSource);

    const box1 = new hitable([82.5,82.5,82.5], [82.5,82.5,82.5], white).transform([130,0,65], 18./180.*Math.PI);
    const box2 = new hitable([82.5,165,82.5], [82.5,165,82.5], white).transform([265,0,295], -15./180.*Math.PI);
    
    shadow_casters.push(box1);
    shadow_casters.push(box2);

    world.push(box1);
    world.push(box2);
}

//
// Ray trace code base on the (excellent) book "Ray tracing in one weekend"[1] by Peter Shirley 
// (@Peter_shirley). 
//
// I have simplified the code a lot and added direct light sampling to reduce noise.
//
// [1] http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html
//
//

function random_cos_weighted_hemisphere_direction(n) {
  	const r = [Math.random(), Math.random()];
	const uu = normalize3(cross3(n, Math.abs(n[1]) > .5 ? [1.,0.,0.] : [0.,1.,0.]));
	const vv = cross3(uu, n);
	const ra = Math.sqrt(r[1]);
	const rx = ra*Math.cos(Math.PI*2*r[0]); 
	const ry = ra*Math.sin(Math.PI*2*r[0]);
	const rz = Math.sqrt(1.-r[1]);
	const rr = add3(add3(scale3(uu,rx), scale3(vv,ry)), scale3(n, rz));
    return normalize3(rr);
}

function rotate_y(p, t) {
    const co = Math.cos(t);
    const si = Math.sin(t);
    return [ p[0]*co+si*p[2], p[1], -p[0]*si+co*p[2]]; 
}

//
// Ray
//

function ray (origin, direction) {
    this.origin = origin;
    this.direction = direction;

    this.translate = function(t) {
        const rt = new ray(this.origin, this.direction);
        rt.origin = sub3(rt.origin, t);
        return rt;
    }

    this.rotate_y = function(t) {
        const rt = new ray(this.origin, this.direction);
        rt.origin = rotate_y(rt.origin, t);
        rt.direction = rotate_y(rt.direction, t);
        return rt;
    }
}

//
// Material
//

function material(albedo, emit, light) {
    this.albedo = albedo;
    this.emit = emit;
    this.light = light;

    this.scatter = function(r_in, rec, attenuation) {
        copy3(attenuation, this.albedo);
        return new ray(rec.p, random_cos_weighted_hemisphere_direction(rec.normal));
    }
    
    this.emitted = function() {
        return this.emit;
    }
}

//
// Hit record
//

function hit_record(p, normal, mat, t) {
    this.p = p;
    this.normal = normal;
    this.mat = mat;
    this.t = t;
    
    this.translate = function(t) {
        const ht = new hit_record(this.p, this.normal, this.mat, this.t);
        ht.p = sub3(ht.p, t);
        return ht;
    }
    
    this.rotate_y = function(t) {
        const ht = new hit_record(this.p, this.normal, this.mat, this.t);
        ht.p = rotate_y(ht.p, t);
        ht.normal = rotate_y(ht.normal, t);
        return ht;
    }
}

//
// Hitable, always box
//

function hitable(center, dimension, mat) {
    this.center = center;
    this.dimension = dimension;
    this.mat = mat;
    this.transformed = false;
    
    this.intersect = function(r, t_min, t_max, rec) {
        if (this.transformed) {
            r = r.translate(this.translate).rotate_y(this.rotate);
        }
        
        const n = [0,0,0];
        const t = this.intersect_box(r, t_min, t_max, n);
        if (t>0) {
            rec.mat = mat;
            rec.normal = n;
            rec.p = add3(r.origin, scale3(r.direction,t)); 
            rec.t = t;
            
            if (this.transformed) {
                const tmp_rec = rec.rotate_y(-this.rotate).translate(
                    [-this.translate[0], -this.translate[1], -this.translate[2]]);
                rec.normal = tmp_rec.normal;
                rec.p = tmp_rec.p;
                rec.t = tmp_rec.t;
            }
            
            return true;
        }
        return false;
    }

    this.intersect_box = function(r, t_min, t_max, normal) {
        const m = [1/r.direction[0], 1/r.direction[1], 1/r.direction[2]];
        const n = mul3(m, sub3(r.origin, this.center));
        const k = mul3(abs3(m),this.dimension);
    	
        const t1 = sub3(scale3(n,-1), k);
        const t2 = add3(scale3(n,-1), k);
    
    	const tN = Math.max( Math.max( t1[0], t1[1] ), t1[2] );
    	const tF = Math.min( Math.min( t2[0], t2[1] ), t2[2] );

    	if( tN > tF || tF < 0.) return -1;
        
        const t = tN < t_min ? tF : tN;
        if (t < t_max && t > t_min) {
    		copy3(normal, scale3(mul3(mul3(sign3(
    		    r.direction),
    		    step3([t1[1],t1[2],t1[0]],t1)),
    		    step3([t1[2],t1[0],t1[1]],t1)),
    		    -1));
    	    return t;
        } else {
            return  -1;
        }
    }
    
    this.transform = function(t, r) {
        this.transformed = true;
        this.translate = t;
        this.rotate = r;
        return this;
    }
}
//
// Camera
//

function camera(lookfrom, lookat, vup, vfov, aspect) {
    const theta = vfov*Math.PI/180.;
    const half_height = Math.tan(theta/2.);
    const half_width = aspect * half_height;
    
    this.origin = lookfrom;
    this.w = normalize3(sub3(lookat, lookfrom));
    this.u = normalize3(cross3(vup, this.w));
    this.v = cross3(this.w, this.u);
    this.horizontal = scale3(this.u,-half_width);
    this.vertical = scale3(this.v,-half_height);
    
    this.get_ray = function(uv) {
        return new ray(this.origin, normalize3(add3(add3(
            scale3(this.horizontal, uv[0]), 
            scale3(this.vertical, uv[1])), 
            this.w)));
    }
}
   
//
// Color & Scene
// 

function world_hit(r, t_min, t_max, rec) {
    let hit = false;
    let closest_so_far  = t_max;
    for (let i=0; i<world.length; i++) {
        if(world[i].intersect(r, t_min, closest_so_far, rec)) {
            closest_so_far = rec.t;
            hit = true;
        }
    }
    return hit;
}

function shadow_hit(r, t_min, t_max) {
    const rec = new hit_record([0,0,0], [0,0,0], false, 0);
    let closest_so_far  = t_max;
    for (let i=0; i<shadow_casters.length; i++) {
        if(shadow_casters[i].intersect(r, t_min, closest_so_far, rec)) {
            return true;
        }
    }
    return false;
}

function color(r) {
    let col = [0,0,0];
    let emitted = [0,0,0];
    const attenuation = [0,0,0];
    
    for (let i=0; i<MAX_RECURSION; i++) {
        const rec = new hit_record([0,0,0], [0,0,0], false, 0);
        
    	if (world_hit(r, 0.01, 10000, rec)) {
            const emit = rec.mat.emitted();
            
            if (rec.mat.light) { // direct light sampling code
                return i == 0 ? emit : emitted;
            }
            
            emitted = add3(emitted, i == 0 ? emit : mul3(col, emit));

            const scattered = rec.mat.scatter(r, rec, attenuation);
            col = i == 0 ? attenuation : mul3(col, attenuation);
            
            // direct light sampling
            const pointInSource = add3(mul3(
                [(Math.random()*2-1), (Math.random()*2-1), (Math.random()*2-1)], 
                lightSource.dimension),
                lightSource.center);
            let L = sub3(pointInSource, rec.p);
            const rr = dot3(L, L);
            const rl = Math.sqrt(rr);
            L = scale3(L, 1/rl);
            
            const shadowRay = new ray(add3(rec.p, scale3(rec.normal,0.01)), L);
            
            if (dot3(rec.normal,L) > 0 && !shadow_hit(shadowRay, .01, 10000)) {
                const area = lightSource.dimension[0] * lightSource.dimension[2] * 4;
                const weight = area / rr * L[1] * dot3(rec.normal, L)  / 3.14159265359;
                emitted = add3(emitted, scale3(mul3(col,lightSource.mat.emitted()),weight));
            }
            r = scattered;
	    } else {
            return emitted;
    	}
        if(dot3(col,col) < 0.0001) {
            return emitted; // optimisation
        }
    }
    return emitted;
}

const scale3=(a,b)=>[a[0]*b,a[1]*b,a[2]*b];
const len3=(a)=>Math.sqrt(dot3(a,a));
const normalize3=(a)=>scale3(a,1/len3(a));
const add3=(a,b)=>[a[0]+b[0],a[1]+b[1],a[2]+b[2]];
const sub3=(a,b)=>[a[0]-b[0],a[1]-b[1],a[2]-b[2]];
const dot3=(a,b)=>a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
const mul3=(a,b)=>[a[0]*b[0],a[1]*b[1],a[2]*b[2]];
const abs3=(a)=>[Math.abs(a[0]),Math.abs(a[1]),Math.abs(a[2])];
const copy3=(a,b)=>{a[0]=b[0],a[1]=b[1],a[2]=b[2]};
const cross3=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];
const sign3=(a)=>[Math.sign(a[0]),Math.sign(a[1]),Math.sign(a[2])];
const step3=(a,b)=>[a[0]<b[0]?1:0,a[1]<b[1]?1:0,a[2]<b[2]?1:0];