// Raytraced Sphere #5. Created by Reinder Nijhoff 2019 // @reindernijhoff // // https://turtletoy.net/turtle/186c7c9e4f // // Forked from "Raytraced sphere #4" by reinder // https://turtletoy.net/turtle/121df29c5c Canvas.setpenopacity(1); const canvas_size = 95; const light_position = [-2,3,-4]; const ro = [0,0,-3.5]; const sphere_pos = [-.2,0,0]; const max_radius = 2; const min_radius = .55; const radius_decr = .9; const max_tries = 400; const circle_radius = .5; const circle_buckets = []; const circle_num_buckets = canvas_size/max_radius; const coords = []; const line_segments = []; let delaunay; let radius = max_radius; const circles = []; const turtle = new Turtle(); function walk(i) { if (i == 0) { do { if(!add_circle(turtle, radius)) { radius *= radius_decr; } } while(radius >= min_radius); delaunay = Delaunator.from(coords); } const t0i = i; const t0 = (t0i/3|0) * 3; const t1i = delaunay.halfedges[i]; const t = delaunay.triangles; const c = delaunay.coords; if (t1i >= t0i) { const t1 = (t1i/3|0) * 3; const p0 = circumcenter(c[t[t0+0]*2+0], c[t[t0+0]*2+1], c[t[t0+1]*2+0], c[t[t0+1]*2+1], c[t[t0+2]*2+0], c[t[t0+2]*2+1]); const p1 = circumcenter(c[t[t1+0]*2+0], c[t[t1+0]*2+1], c[t[t1+1]*2+0], c[t[t1+1]*2+1], c[t[t1+2]*2+0], c[t[t1+2]*2+1]); turtle.jump(p0.x, p0.y); turtle.goto(p1.x, p1.y); } else if (t1i < 0) { const p0 = circumcenter(c[t[t0+0]*2+0], c[t[t0+0]*2+1], c[t[t0+1]*2+0], c[t[t0+1]*2+1], c[t[t0+2]*2+0], c[t[t0+2]*2+1]); const v0 = [c[t[t0+(t0i % 3)]*2+0], c[t[t0+(t0i % 3)]*2+1]]; const v1 = [c[t[t0+((t0i + 1) % 3)]*2+0], c[t[t0+((t0i + 1) % 3)]*2+1]]; const d = [(v0[1]-v1[1]), -(v0[0]-v1[0])]; const l = Math.sqrt(d[0]**2 + d[1]**2); turtle.jump(p0.x, p0.y); turtle.goto(p0.x + 200*d[0]/l, p0.y + 200*d[1]/l); } return i < delaunay.halfedges.length - 1; } for (let i=0; i<circle_num_buckets+2; i++) { circle_buckets[i]=[]; for (let j=0; j<circle_num_buckets+2; j++) { circle_buckets[i][j] = []; } } function add_circle(t, r) { let coord_found = false; let tries = 0; const drdr = r*r*2; while (!coord_found && tries < max_tries) { tries ++; const x = Math.random() * (canvas_size-r)*2 -canvas_size + r; const y = Math.random() * (canvas_size-r)*2 -canvas_size + r; let possible = true; const xb = Math.max(0,((.5*x/canvas_size+.5)*circle_num_buckets)|0); const yb = Math.max(0,((.5*y/canvas_size+.5)*circle_num_buckets)|0); for (let xbi = Math.max(0,xb-1); xbi<xb+2 && possible; xbi++) { for (let ybi = Math.max(0,yb-1); ybi<yb+2 && possible; ybi++) { const circles = circle_buckets[xbi][ybi]; for (let i=0; i<circles.length && possible; i++) { const dx = circles[i][0] - x; const dy = circles[i][1] - y; if ( dx*dx + dy*dy < drdr) { possible = false; break; } } } } if (possible) { coord_found = true; draw_circle(x,y,t, r); circle_buckets[xb][yb].push([x,y]); return true; } } return false; } function draw_circle(x,y,t,r) { const intensity = get_image_intensity(x/canvas_size, y/canvas_size); // use intensity squared because it looks better if ((r-min_radius)/max_radius > .65 * intensity * intensity) { coords.push([x,y]); } } function get_image_intensity(x,y) { const rd = normalize3([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 = add3(ro, scale3(rd, dist)); normal = normalize3(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 = add3(ro, scale3(rd, dist)); normal = [0,1,0]; } } if (dist > 0 && dist < 100) { let vec_to_light = sub3(hit, light_position); const light_dist_sqr = dot3(vec_to_light, vec_to_light); vec_to_light = scale3(vec_to_light, -1/Math.sqrt(light_dist_sqr)); let light = dot3(normal, vec_to_light); light *= 30 / light_dist_sqr; // shadow ? if (plane_hit && intersect_sphere(hit, vec_to_light, sphere_pos, 1) > 0) { light = 0; } return Math.sqrt(Math.min(1, Math.max(0,light))); } else { return 0; } } 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]; function intersect_sphere(ro, rd, center, radius) { const oc = sub3(ro, center); const b = dot3( oc, rd ); const c = dot3( oc, oc ) - radius * radius; const h = b*b - c; if( h<0 ) return -1; return -b - Math.sqrt( h ); } // // https://github.com/mapbox/delaunator // // ISC License // // Copyright (c) 2017, Mapbox // // Permission to use, copy, modify, and/or distribute this software for any purpose // with or without fee is hereby granted, provided that the above copyright notice // and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS // OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER // TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF // THIS SOFTWARE. // const EPSILON = Math.pow(2, -52); const EDGE_STACK = new Uint32Array(512); class Delaunator { static from(points, getX = defaultGetX, getY = defaultGetY) { const n = points.length; const coords = new Float64Array(n * 2); for (let i = 0; i < n; i++) { const p = points[i]; coords[2 * i] = getX(p); coords[2 * i + 1] = getY(p); } return new Delaunator(coords); } constructor(coords) { const n = coords.length >> 1; if (n > 0 && typeof coords[0] !== 'number') throw new Error('Expected coords to contain numbers.'); this.coords = coords; // arrays that will store the triangulation graph const maxTriangles = 2 * n - 5; const triangles = this.triangles = new Uint32Array(maxTriangles * 3); const halfedges = this.halfedges = new Int32Array(maxTriangles * 3); // temporary arrays for tracking the edges of the advancing convex hull this._hashSize = Math.ceil(Math.sqrt(n)); const hullPrev = this.hullPrev = new Uint32Array(n); // edge to prev edge const hullNext = this.hullNext = new Uint32Array(n); // edge to next edge const hullTri = this.hullTri = new Uint32Array(n); // edge to adjacent triangle const hullHash = new Int32Array(this._hashSize).fill(-1); // angular edge hash // populate an array of point indices; calculate input data bbox const ids = new Uint32Array(n); let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (let i = 0; i < n; i++) { const x = coords[2 * i]; const y = coords[2 * i + 1]; if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; ids[i] = i; } const cx = (minX + maxX) / 2; const cy = (minY + maxY) / 2; let minDist = Infinity; let i0, i1, i2; // pick a seed point close to the center for (let i = 0; i < n; i++) { const d = dist(cx, cy, coords[2 * i], coords[2 * i + 1]); if (d < minDist) { i0 = i; minDist = d; } } const i0x = coords[2 * i0]; const i0y = coords[2 * i0 + 1]; minDist = Infinity; // find the point closest to the seed for (let i = 0; i < n; i++) { if (i === i0) continue; const d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1]); if (d < minDist && d > 0) { i1 = i; minDist = d; } } let i1x = coords[2 * i1]; let i1y = coords[2 * i1 + 1]; let minRadius = Infinity; // find the third point which forms the smallest circumcircle with the first two for (let i = 0; i < n; i++) { if (i === i0 || i === i1) continue; const r = circumradius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1]); if (r < minRadius) { i2 = i; minRadius = r; } } let i2x = coords[2 * i2]; let i2y = coords[2 * i2 + 1]; if (minRadius === Infinity) { throw new Error('No Delaunay triangulation exists for this input.'); } // swap the order of the seed points for counter-clockwise orientation if (orient(i0x, i0y, i1x, i1y, i2x, i2y)) { const i = i1; const x = i1x; const y = i1y; i1 = i2; i1x = i2x; i1y = i2y; i2 = i; i2x = x; i2y = y; } const center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y); this._cx = center.x; this._cy = center.y; const dists = new Float64Array(n); for (let i = 0; i < n; i++) { dists[i] = dist(coords[2 * i], coords[2 * i + 1], center.x, center.y); } // sort the points by distance from the seed triangle circumcenter quicksort(ids, dists, 0, n - 1); // set up the seed triangle as the starting hull this.hullStart = i0; let hullSize = 3; hullNext[i0] = hullPrev[i2] = i1; hullNext[i1] = hullPrev[i0] = i2; hullNext[i2] = hullPrev[i1] = i0; hullTri[i0] = 0; hullTri[i1] = 1; hullTri[i2] = 2; hullHash[this._hashKey(i0x, i0y)] = i0; hullHash[this._hashKey(i1x, i1y)] = i1; hullHash[this._hashKey(i2x, i2y)] = i2; this.trianglesLen = 0; this._addTriangle(i0, i1, i2, -1, -1, -1); for (let k = 0, xp, yp; k < ids.length; k++) { const i = ids[k]; const x = coords[2 * i]; const y = coords[2 * i + 1]; // skip near-duplicate points if (k > 0 && Math.abs(x - xp) <= EPSILON && Math.abs(y - yp) <= EPSILON) continue; xp = x; yp = y; // skip seed triangle points if (i === i0 || i === i1 || i === i2) continue; // find a visible edge on the convex hull using edge hash let start = 0; for (let j = 0, key = this._hashKey(x, y); j < this._hashSize; j++) { start = hullHash[(key + j) % this._hashSize]; if (start !== -1 && start !== hullNext[start]) break; } start = hullPrev[start]; let e = start, q; while (q = hullNext[e], !orient(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1])) { e = q; if (e === start) { e = -1; break; } } if (e === -1) continue; // likely a near-duplicate point; skip it // add the first triangle from the point let t = this._addTriangle(e, i, hullNext[e], -1, -1, hullTri[e]); // recursively flip triangles from the point until they satisfy the Delaunay condition hullTri[i] = this._legalize(t + 2); hullTri[e] = t; // keep track of boundary triangles on the hull hullSize++; // walk forward through the hull, adding more triangles and flipping recursively let n = hullNext[e]; while (q = hullNext[n], orient(x, y, coords[2 * n], coords[2 * n + 1], coords[2 * q], coords[2 * q + 1])) { t = this._addTriangle(n, i, q, hullTri[i], -1, hullTri[n]); hullTri[i] = this._legalize(t + 2); hullNext[n] = n; // mark as removed hullSize--; n = q; } // walk backward from the other side, adding more triangles and flipping if (e === start) { while (q = hullPrev[e], orient(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1])) { t = this._addTriangle(q, i, e, -1, hullTri[e], hullTri[q]); this._legalize(t + 2); hullTri[q] = t; hullNext[e] = e; // mark as removed hullSize--; e = q; } } // update the hull indices this.hullStart = hullPrev[i] = e; hullNext[e] = hullPrev[n] = i; hullNext[i] = n; // save the two new edges in the hash table hullHash[this._hashKey(x, y)] = i; hullHash[this._hashKey(coords[2 * e], coords[2 * e + 1])] = e; } this.hull = new Uint32Array(hullSize); for (let i = 0, e = this.hullStart; i < hullSize; i++) { this.hull[i] = e; e = hullNext[e]; } this.hullPrev = this.hullNext = this.hullTri = null; // get rid of temporary arrays // trim typed triangle mesh arrays this.triangles = triangles.subarray(0, this.trianglesLen); this.halfedges = halfedges.subarray(0, this.trianglesLen); } _hashKey(x, y) { return Math.floor(pseudoAngle(x - this._cx, y - this._cy) * this._hashSize) % this._hashSize; } _legalize(a) { const {triangles, coords, halfedges} = this; let i = 0; let ar = 0; // recursion eliminated with a fixed-size stack while (true) { const b = halfedges[a]; /* if the pair of triangles doesn't satisfy the Delaunay condition * (p1 is inside the circumcircle of [p0, pl, pr]), flip them, * then do the same check/flip recursively for the new pair of triangles * * pl pl * /||\ / \ * al/ || \bl al/ \a * / || \ / \ * / a||b \ flip /___ar___\ * p0\ || /p1 => p0\---bl---/p1 * \ || / \ / * ar\ || /br b\ /br * \||/ \ / * pr pr */ const a0 = a - a % 3; ar = a0 + (a + 2) % 3; if (b === -1) { // convex hull edge if (i === 0) break; a = EDGE_STACK[--i]; continue; } const b0 = b - b % 3; const al = a0 + (a + 1) % 3; const bl = b0 + (b + 2) % 3; const p0 = triangles[ar]; const pr = triangles[a]; const pl = triangles[al]; const p1 = triangles[bl]; const illegal = inCircle( coords[2 * p0], coords[2 * p0 + 1], coords[2 * pr], coords[2 * pr + 1], coords[2 * pl], coords[2 * pl + 1], coords[2 * p1], coords[2 * p1 + 1]); if (illegal) { triangles[a] = p1; triangles[b] = p0; const hbl = halfedges[bl]; // edge swapped on the other side of the hull (rare); fix the halfedge reference if (hbl === -1) { let e = this.hullStart; do { if (this.hullTri[e] === bl) { this.hullTri[e] = a; break; } e = this.hullNext[e]; } while (e !== this.hullStart); } this._link(a, hbl); this._link(b, halfedges[ar]); this._link(ar, bl); const br = b0 + (b + 1) % 3; // don't worry about hitting the cap: it can only happen on extremely degenerate input if (i < EDGE_STACK.length) { EDGE_STACK[i++] = br; } } else { if (i === 0) break; a = EDGE_STACK[--i]; } } return ar; } _link(a, b) { this.halfedges[a] = b; if (b !== -1) this.halfedges[b] = a; } // add a new triangle given vertex indices and adjacent half-edge ids _addTriangle(i0, i1, i2, a, b, c) { const t = this.trianglesLen; this.triangles[t] = i0; this.triangles[t + 1] = i1; this.triangles[t + 2] = i2; this._link(t, a); this._link(t + 1, b); this._link(t + 2, c); this.trianglesLen += 3; return t; } } // monotonically increases with real angle, but doesn't need expensive trigonometry function pseudoAngle(dx, dy) { const p = dx / (Math.abs(dx) + Math.abs(dy)); return (dy > 0 ? 3 - p : 1 + p) / 4; // [0..1] } function dist(ax, ay, bx, by) { const dx = ax - bx; const dy = ay - by; return dx * dx + dy * dy; } function orient(px, py, qx, qy, rx, ry) { return (qy - py) * (rx - qx) - (qx - px) * (ry - qy) < 0; } function inCircle(ax, ay, bx, by, cx, cy, px, py) { const dx = ax - px; const dy = ay - py; const ex = bx - px; const ey = by - py; const fx = cx - px; const fy = cy - py; const ap = dx * dx + dy * dy; const bp = ex * ex + ey * ey; const cp = fx * fx + fy * fy; return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) < 0; } function circumradius(ax, ay, bx, by, cx, cy) { const dx = bx - ax; const dy = by - ay; const ex = cx - ax; const ey = cy - ay; const bl = dx * dx + dy * dy; const cl = ex * ex + ey * ey; const d = 0.5 / (dx * ey - dy * ex); const x = (ey * bl - dy * cl) * d; const y = (dx * cl - ex * bl) * d; return x * x + y * y; } function circumcenter(ax, ay, bx, by, cx, cy) { const dx = bx - ax; const dy = by - ay; const ex = cx - ax; const ey = cy - ay; const bl = dx * dx + dy * dy; const cl = ex * ex + ey * ey; const d = 0.5 / (dx * ey - dy * ex); const x = ax + (ey * bl - dy * cl) * d; const y = ay + (dx * cl - ex * bl) * d; return {x, y}; } function quicksort(ids, dists, left, right) { if (right - left <= 20) { for (let i = left + 1; i <= right; i++) { const temp = ids[i]; const tempDist = dists[temp]; let j = i - 1; while (j >= left && dists[ids[j]] > tempDist) ids[j + 1] = ids[j--]; ids[j + 1] = temp; } } else { const median = (left + right) >> 1; let i = left + 1; let j = right; swap(ids, median, i); if (dists[ids[left]] > dists[ids[right]]) swap(ids, left, right); if (dists[ids[i]] > dists[ids[right]]) swap(ids, i, right); if (dists[ids[left]] > dists[ids[i]]) swap(ids, left, i); const temp = ids[i]; const tempDist = dists[temp]; while (true) { do i++; while (dists[ids[i]] < tempDist); do j--; while (dists[ids[j]] > tempDist); if (j < i) break; swap(ids, i, j); } ids[left + 1] = ids[j]; ids[j] = temp; if (right - i + 1 >= j - left) { quicksort(ids, dists, i, right); quicksort(ids, dists, left, j - 1); } else { quicksort(ids, dists, left, j - 1); quicksort(ids, dists, i, right); } } } function swap(arr, i, j) { const tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } function defaultGetX(p) { return p[0]; } function defaultGetY(p) { return p[1]; }