Raytraced Sphere with Quads

raytracing + quadtree subdivision

Special thanks to @reinder for the raytracing algorithm.

Would have worked on the first pass but rounding errors were making things wrong and ugly.

#raytracer #raytrace #dither #gradient #pixel

Log in to post a comment.

// Forked from "FSD Raytraced Sphere #1" by imakerobots
// https://turtletoy.net/turtle/8f85afd31a

// Forked from "Floyd–Steinberg dithering" by imakerobots



// raytraced sphere part from https://turtletoy.net/turtle/11075dfee0
const canvas_size = 95;
const brown_rot = 360;
const brown_for_min = 1;
const brown_for_max = 10;

const light_position = [-2,3,-4];
const ro = [0,0,-3.5];
const sphere_pos = [-.2,0,0];

function get_image_intensity(x,y) {
    x /= canvas_size;
    y /= canvas_size;
    
    const rd = vec_normalize([x,-y,2]);
    let normal;
    let light = 0;
    let hit;
    let plane_hit = false;
    
    let dist = intersect_sphere(ro, rd, sphere_pos, 1);
    if (dist > 0) {
        hit = vec_add(ro, vec_mul(rd, dist));
        normal = vec_normalize(hit);
    } else {
        dist = 10000;
    }
    if (rd[1] < 0) {
        const plane_dist = -1/rd[1];
       if (plane_dist < dist) {
            dist = plane_dist;
            plane_hit = true;
            hit = vec_add(ro, vec_mul(rd, dist));
            normal = [0,1,0];
        }
    } 
    
    if (dist > 0 && dist < 100) {
        let vec_to_light = vec_sub(hit, light_position);
        const light_dist_sqr = vec_dot(vec_to_light, vec_to_light);
        
        vec_to_light = vec_mul(vec_to_light, -1/Math.sqrt(light_dist_sqr));
        
        let light = vec_dot(normal, vec_to_light);
        light *= 20 / light_dist_sqr;  // changed the intensity a bit
        
        // shadow ?
        if (plane_hit && intersect_sphere(hit, vec_to_light, sphere_pos, 1) > 0) {
            light = 0.01;  // added some ambient light.  make 0 for none.
        }
        
        return Math.sqrt(Math.min(1, Math.max(0,light)));
    } else {
        return 0;
    }
}


// math functions
function vec_normalize(a) {
    const l = Math.sqrt(vec_dot(a,a));
    return [a[0]/l,a[1]/l,a[2]/l];
}

function vec_add(a, b) {
    return [a[0]+b[0], a[1]+b[1], a[2]+b[2]]
}

function vec_mul(a, b) {
    return [a[0]*b, a[1]*b, a[2]*b]
}

function vec_sub(a, b) {
    return [a[0]-b[0], a[1]-b[1], a[2]-b[2]]
}

function vec_dot(a, b) {
    return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];
}

function intersect_sphere(ro, rd, center, radius) {
    const oc = vec_sub(ro, center);
	const b = vec_dot( oc, rd );
	const c = vec_dot( oc, oc ) - radius * radius;
	const h = b*b - c;
	if( h<0 ) return -1;
	return -b - Math.sqrt( h );
}









const iterations=800; // min=1 max=5000, step=10

// dithering part from https://turtletoy.net/turtle/d47e2bad0c
const opacity=0.1;
var brightest;
Canvas.setpenopacity(opacity);

// Global code will be evaluated once.
const turtle = new Turtle();


function get_pixel(x,y) {
    return get_image_intensity( x,y );
}

function get_image(x,y) {
    y=Math.floor(y);
    x=Math.floor(x);
    return image[y*200+x];
}


function QuadTree_create(x0,y0,x1,y1) {
    var quad=[
        Math.floor(x0),
        Math.floor(y0),
        Math.floor(x1),
        Math.floor(y1),
        0,
        0
    ];

    var w=Math.floor(x1-x0);
    var h=Math.floor(y1-y0);
    var size=w*h;
    // find the average
    for(var y=0;y<h;++y) {
        for(var x=0;x<w;++x) {
            quad[4] += get_image(x0+x,y0+y);
        }
    }
    quad[4] /= size;
    // find the mean square error
    for(var y=0;y<h;++y) {
        for(var x=0;x<w;++x) {
            var d = get_image(x0+x,y0+y) - quad[4];
            quad[5] += d*d;
        }
    }
    quad[5] /= size;
    
    return quad;
}

function QuadTree_drawX(quad) {
    var w=(quad[2]+quad[0])/2;
    var h=(quad[3]+quad[1])/2;
    turtle.jump(w      -100,quad[1]-100);
    turtle.goto(w      -100,quad[3]-100);
    turtle.jump(quad[0]-100,h      -100);
    turtle.goto(quad[2]-100,h      -100);
}

function QuadTree_drawBox(quad) {
    turtle.jump(quad[0]-100,quad[1]-100);
    turtle.goto(quad[0]-100,quad[3]-100);
    turtle.goto(quad[2]-100,quad[3]-100);
    turtle.goto(quad[2]-100,quad[1]-100);
    turtle.goto(quad[0]-100,quad[1]-100);
}

function QuadTree_drawShaded(quad) {
    QuadTree_drawBox(quad);
    QuadTree_drawBox(quad);
    var left =quad[0]-100;
    var right=quad[2]-100;
    
    var intensity = 1-(quad[4]/brightest);
    if(intensity>0.01) {
        // fill the quad
        for(var i=0;i<intensity;i+=0.1) {
            for(var y=quad[1];y<quad[3];y+=0.1) {
                turtle.jump(left ,y-100);
                turtle.goto(right,y-100);
            }
        }
    }
}

function QuadTree_split(quad) {
    var w=(quad[2]-quad[0]);
    var h=(quad[3]-quad[1]);
    var w2=w/2;
    var h2=h/2;
    if(w2<=1||h2<=1) {
        finishedNodes.push(quad);
        return;
    }
    //QuadTree_drawX(quad);
    var x0=quad[0];
    var y0=quad[1];
    var x1=quad[2];
    var y1=quad[3];
    allNodes.push(QuadTree_create(x0   ,y0   ,x0+w2,y0+h2));
    allNodes.push(QuadTree_create(x0+w2,y0   ,x1   ,y0+h2));
    allNodes.push(QuadTree_create(x0+w2,y0+h2,x1   ,y1   ));
    allNodes.push(QuadTree_create(x0   ,y0+h2,x0+w2,y1   ));
}

// precalculate the raytraced image.
var image=[];
for(var y=0;y<200;++y) {
    for(var x=0;x<200;++x) {
        image[y*200+x] = get_pixel(x-100,y-100);
    }
}

// our list of active quads
var allNodes = [];
var finishedNodes = [];
// initialized with the root quad
allNodes.push(QuadTree_create(0,0,200,200));

// draw the outside quad
turtle.pendown();
turtle.jump(-100,-100);
turtle.goto(-100, 100);
turtle.goto( 100, 100);
turtle.goto( 100,-100);
turtle.goto(-100,-100);
//drawOriginal();

function drawOriginal() {
    // display the raytraced image
    for(var py=0;py<200;++py) {
        for(var px=0;px<200;++px) {
            if(image[py*200+px]<0.65) {
                turtle.jump(px-100,py-100);
                turtle.seth(45);
                turtle.forward(1);
            }
            if(image[py*200+px]<0.15) {
                turtle.jump(px-100,py-100);
                turtle.seth(45+90);
                turtle.forward(1);
            }
        }
    }
}

function walk(i) {
    // find the quadtree node with the biggest error  
    var worst=0;
    for(var j=1;j<allNodes.length;++j) {
        if(allNodes[worst][5] < allNodes[j][5]) {
            worst=j;
        }
    }
    // split the quad node and draw the split
    QuadTree_split(allNodes[worst]);
    // remember the error (maybe quit when error small enough?)
    var err=allNodes[worst][5];
    // take the worst off the list
    allNodes.splice(worst,1);

    if(i==iterations) {
        drawAllRemainingNodes();
        return false;
    }
    return true;
}

function drawAllRemainingNodes() {
    brightest=0;
    for(var j=0;j<allNodes.length;++j) {
        if(brightest<allNodes[j][4]) {
            brightest=allNodes[j][4];
        }
    }
    for(var j=0;j<finishedNodes.length;++j) {
        if(brightest<finishedNodes[j][4]) {
            brightest=finishedNodes[j][4];
        }
    }
    for(var j=0;j<allNodes.length;++j) {
        QuadTree_drawShaded(allNodes[j]);
    }
    for(var j=0;j<finishedNodes.length;++j) {
        QuadTree_drawShaded(finishedNodes[j]);
    }
}