FSD Raytraced Glass Sphere

A combination of Floyd-Steinberg Dithering and raytracing with reflection, refraction, and fresnel lensing. There is no supersampling to get the light that bends through the glass and hits the floor.

#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 sphere_pos = [0,1,0];

function cast_ray(origin,rd,depth) {
    depth--;
    if(depth<=0) return 0;
    
    light = 0;
    plane_hit = false;
    
    dist = intersect_sphere(origin, rd, sphere_pos, 1);
    if (dist > 0) {
        hit = vec_add(origin, vec_mul(rd, dist));
        normal = vec_normalize(vec_sub(hit,sphere_pos));
    } else {
        dist = 10000;
    }
    if (rd[1] < 0) {
       const plane_dist = origin[1]/-rd[1];
       if (plane_dist>0 && plane_dist < dist) 
       {
            dist = plane_dist;
            plane_hit = true;
            hit = vec_add(origin, vec_mul(rd, dist));
            normal = [0,1,0];
        }
    }
    
    if (dist > 0.00001 && 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) {
            if(intersect_sphere(hit, vec_to_light, sphere_pos, 1) > 0) {
                light = 0.01;  // added some ambient light.  make 0 for none.
            }
            xodd = (hit[0]-Math.floor(hit[0]))>0.5;
            zodd = (hit[2]-Math.floor(hit[2]))>0.5;
            if( xodd==zodd) {
                light/=25;
            }
        } else {
            var cosi=Math.max(-1,Math.min(1,vec_dot(normal,rd)));

            // fresnel effect
            kr = 1; 
            // materials (for index of refraction)
            var etai = 1;
            var etat = 1.3; 
            outside=true;
            if (cosi < 0) {
                cosi = -cosi;
            } else {
                outside=false;
                et = etai;
                etai=etat;
                etat=et;
            }
            

            // reflected ray
            reflected_ray=vec_normalize(vec_add(rd,vec_mul(normal,cosi*2)));
            reflected=cast_ray(hit,reflected_ray,depth);

            // get refracted ray
            refracted=0;
            // Compute sini using Snell's law
            sint = etai / etat * Math.sqrt(Math.max(0, 1.0 - cosi * cosi)); 
            if (sint <1) {
                // not Total internal reflection
                cost = Math.sqrt(Math.max(0, 1 - sint * sint)); 
                Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost)); 
                Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost)); 
                kr = (Rs * Rs + Rp * Rp) / 2; 
            }
            if(kr<1) {
                eta = etai / etat;
                k = 1 - eta * eta * (1 - cosi * cosi); 
                if( k >= 0 ) {
                    refracted_ray=vec_normalize(
                        vec_add(
                            vec_mul(rd, eta),
                            vec_mul(normal, eta * cosi - Math.sqrt(k) )
                        )
                    );
                    refracted=cast_ray(hit,refracted_ray,depth);
                }
            }
            
            //kr=0;
            //light= 0;
            light=Math.max(0,light);
            light=Math.pow(light,26)*0.0008;  // a little phong style intensity on the spot
            light+= reflected*kr;
            light+= refracted*(1-kr);
        }
        
        return Math.sqrt(Math.min(1, Math.max(0,light)));
    } else {
        return 0;
    }
}

function get_image_intensity(x,y) {
    x /= canvas_size;
    y /= canvas_size;
    
    const camera = [0.4,1,-3.];
    const rd = vec_normalize([x,-y,2]);
    return cast_ray(camera,rd,5);
}


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

//https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-sphere-intersection
function intersect_sphere(ro, rd, center, radius) {
    const L = vec_sub(center, ro);
	const tca = vec_dot( L, rd );
	const d2 = vec_dot( L, L ) - tca*tca;
	radius2=radius*radius;
	if(d2>radius2) return -1;
    thc = Math.sqrt(radius2 - d2); 
    t0 = tca - thc; 
    t1 = tca + thc;
    if(t1<t0) {
        t2=t1;
        t1=t0;
        t0=t2;
    }
    if(t0<0) {
        t0=t1;
        if(t0<0) {
            return -1;
        }
    }
    return t0;
}










// 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) {
    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;
}