Log in to post a comment.
// Forked from "Cornell Box #1" by reinder const MAX_RECURSION = 3; // max=10, min=1, step=1 const SAMPLES_PER_PIXEL = 3; // max=15, min=1, step=1 const canvas_size = 100; let cam; let world = []; let shadow_casters = []; let lightSource; function get_image_intensity(x,y) { x/=canvas_size; y/=canvas_size; l = 0; for (i=0; i<SAMPLES_PER_PIXEL; i++) { const r = cam.get_ray([x, y]); const col = color(r); l += dot3(col, [2.299, 2.587, 2.114]); } // v = Math.pow(l/SAMPLES_PER_PIXEL, 1.0/1.5); v = l/SAMPLES_PER_PIXEL; if(v<0) v=0; if(v>1) v=1; return v; } // // 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]; // dithering part from https://turtletoy.net/turtle/d47e2bad0c Canvas.setpenopacity(1); // Global code will be evaluated once. const turtle = new Turtle(); const level_of_detail=2; // min=1.5, max=20, step=0.5 turtle.penup(); turtle.goto(-100,-100); const stepSize = level_of_detail/10.0; const stepsPerLine = Math.floor(200.0/stepSize); stepsRemaining=stepsPerLine*stepsPerLine; dx=1; error1 = new Array(stepsPerLine); error2 = new Array(stepsPerLine); for(i=0;i<error1.length;++i) { error1[i] = error2[i] = 0; } function newLine() { for(var i=0;i<stepsPerLine;++i) { error1[i] = error2[i]; error2[i] = 0; } } function get_pixel(x,y) { return get_image_intensity( x,y ); } function find_closest_palette_color(oldpixel) { return oldpixel>0.5?1.0:0.0; } function walk(i) { if(i==0) init(); var x=turtle.x()/stepSize+stepsPerLine/2; var y=turtle.y()/stepSize+stepsPerLine/2; if(x<0) x=0; if(x>=stepsPerLine) x=stepsPerLine-1; x=Math.floor(x); var oldpixel = error1[x] + get_pixel(turtle.x(),turtle.y()); var newpixel = find_closest_palette_color(oldpixel); var quant_error = oldpixel - newpixel; var xP1 = x+dx; var xM1 = x-dx; if(xP1>=0 && xP1<stepsPerLine) error1[xP1] += quant_error * 7.0/16.0; if(xM1>=0 && xM1<stepsPerLine) error2[xM1] += quant_error * 3.0/16.0; error2[x ] += quant_error * 5.0/16.0; if(xP1>=0 && xP1<stepsPerLine) error2[xP1] += quant_error * 1.0/16.0; if(newpixel>0.5) { turtle.penup(); } else { turtle.pendown(); } turtle.forward(stepSize); if(turtle.x()>=100) { turtle.right(90); turtle.forward(stepSize); turtle.right(90); dx=-dx; newLine(); } else if(turtle.x()<=-100) { turtle.left(90); turtle.forward(stepSize); turtle.left(90); dx=-dx; newLine(); } return turtle.y()<100 && turtle.y()>=-100;//stepsRemaining-- > 0; }