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