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