### Cornell Box #2

Path tracer code from Cornell Box #1 combined with the 'circle packing dithering' from Raytraced Sphere #3.

#raytracer #pixels #rays #dithering

```// Cornell Box #2. Created by Reinder Nijhoff 2019
// @reindernijhoff
//
// https://turtletoy.net/turtle/4fc71843ee
//
// 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
//

Canvas.setpenopacity(1);

const MAX_RECURSION = 3;
const SAMPLES_PER_PIXEL = 8;

const canvas_size = 102;

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

//
// Init all turtles used for hatching
//

const max_tries = 250;
const circle_buckets = [];

const circles = [];

const turtle = new Turtle();

for (let i=0; i<circle_num_buckets+2; i++) {
circle_buckets[i]=[];
for (let j=0; j<circle_num_buckets+2; j++) {
circle_buckets[i][j] = [];
}
}

let coord_found = false;
let tries = 0;
const drdr = r*r*2;

while (!coord_found && tries < max_tries) {
tries ++;
const x = Math.random() * (canvas_size-r)*2 -canvas_size + r;
const y = Math.random() * (canvas_size-r)*2 -canvas_size + r;
let possible = true;

const xb = Math.max(0,((.5*x/canvas_size+.5)*circle_num_buckets)|0);
const yb = Math.max(0,((.5*y/canvas_size+.5)*circle_num_buckets)|0);

for (let xbi = Math.max(0,xb-1); xbi<xb+2 && possible; xbi++) {
for (let ybi = Math.max(0,yb-1); ybi<yb+2 && possible; ybi++) {
const circles = circle_buckets[xbi][ybi];
for (let i=0; i<circles.length && possible; i++) {
const dx = circles[i][0] - x;
const dy = circles[i][1] - y;

if ( dx*dx + dy*dy < drdr) {
possible = false;
break;
}
}
}
}
if (possible) {
coord_found = true;
draw_circle(x,y,t, r);
circle_buckets[xb][yb].push([x,y]);
return true;
}
}
return false;
}

function draw_circle(x,y,t,r) {
const intensity = get_image_intensity(x/canvas_size, y/canvas_size);
turtle.penup();
turtle.pendown();
}
}

function walk(i) {
if (i==0) {
init();
}
}
}

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

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

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++) {
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
[(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 area = lightSource.dimension[0] * lightSource.dimension[2] * 4;
const weight = area / rr * L[1] * dot3(rec.normal, L) / 3.14159265359;
}
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 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];```