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