FSD Cornell Box #1

Floyd-Steinberg Dithering of Cornell Box #1 (originally by @reinder)

#raytracer #raytrace #dither #gradient #pixel

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